How to pass a JavaScript function to another process over JSON? - javascript

I'm doing a chrome extension -- how would I pass a function from background to a content-script instance, obviously the function needs to be serialized?
Is it just okay to do eval(function_string)?
This is my first time practicing such voodoo, help invited.

Here is the code I used to achieve this :
The custom scripting language script containing functions to be sent :
// An example script
// Showing how dependent functions are defined later on
// With functions that have no dependencies grouped together in an associative array
// preceding, in the execution_context_loader's list, the functions that depend on them.
var collection_script.execution_context_loader = [
{
fa:function(a) { return a+1; },
fb:function(b) { return b*2; },
fc:function(c) { return c-3; }
},
{
fabc:function(a,b,c) {
return window.qwontical.execution_context.fa(a)^
window.qwontical.execution_context.fb(b)^
window.qwontical.execution_context.fc(c);
}
}
];
var script_point = {
name : 'alpha',
source : '/root/space',
data_args : [ '/j1', '/j2', '/j3' ],
function_applied : 'fabc'
};
Which when executed on the other side (say by calling some function execute("alpha");) will apply the function fabc (serialized and revivified) to the resources found at /root/space/j1, and also .../j2 and .../j3.
On the function sending side :
// Function to package a script's execution context for dispatch to a collection script
function package_script_for_dispatch( collection_script ) {
console.log(collection_script);
if ( !!collection_script.execution_context_loader ) {
for ( var i = 0; i < collection_script.execution_context_loader.length; i += 1) {
for ( var func_def in collection_script.execution_context_loader[i] ) {
collection_script.execution_context_loader[i][func_def] = collection_script.execution_context_loader[i][func_def].toString();
console.log("Packed : " + collection_script.execution_context_loader[i][func_def]);
}
}
} else {
collection_script.execution_context_loader = {};
}
}
On the function receiving side :
for( var i = 0; i < collectionscript.execution_context_loader.length; i+= 1) {
for (var func_name in collectionscript.execution_context_loader[i] ) {
var func_def_string = collectionscript.execution_context_loader[i][func_name];
console.log("About to revivify " + func_name + " : " + func_def_string);
eval("window.qwontical.execution_context['"+func_name+"'] = "+func_def_string);
}
}
The calling convention for such a function is then
window.qwontical.execution_context[func_name](args...);
This enables functions to be defined in a script by their name and then used later on, after that script has been serialized and "revivified"

Related

Trying to convert existing synchronous XmlHttpRequest object to be asynchronous to keep up with current fads

Soo, I keep getting slammed with cautions from Chrome about how synchronous XmlHttpRequest calls are being deprecated, and I've decided to have a go at trying to convert my use-case over in order to keep up with this fad...
In this case, I have an ~9 year old JS object that has been used as the central (and exemplary) means of transporting data between the server and our web-based applications using synchronous XHR calls. I've created a chopped-down version to post here (by gutting out a lot of sanity, safety and syntax checking):
function GlobalData()
{
this.protocol = "https://";
this.adminPHP = "DataMgmt.php";
this.ajax = false;
this.sessionId = "123456789AB";
this.validSession = true;
this.baseLocation = "http://www.example.com/";
this.loadResult = null;
this.AjaxPrep = function()
{
this.ajax = false;
if (window.XMLHttpRequest) {
try { this.ajax = new XMLHttpRequest(); } catch(e) { this.ajax = false; } }
}
this.FetchData = function (strUrl)
{
if ((typeof strURL=='string') && (strURL.length > 0))
{
if (this.ajax === false)
{
this.AjaxPrep();
if (this.ajax === false) { alert('Unable to initialise AJAX!'); return ""; }
}
strURL = strURL.replace("http://",this.protocol); // We'll only ask for data from secure (encrypted-channel) locations...
if (strURL.indexOf(this.protocol) < 0) strURL = this.protocol + this.adminPHP + strURL;
strURL += ((strURL.indexOf('?')>= 0) ? '&' : '?') + 'dynamicdata=' + Math.floor(Math.random() * this.sessionId);
if (this.validSession) strURL += "&sessionId=" + this.sessionId;
this.ajax.open("GET", strURL, false);
this.ajax.send();
if (this.ajax.status==200) strResult = this.ajax.responseText;
else alert("There was an error attempting to communicate with the server!\r\n\r\n(" + this.ajax.status + ") " + strURL);
if (strResult == "result = \"No valid Session information was provided.\";")
{
alert('Your session is no longer valid!');
window.location.href = this.baseLocation;
}
}
else console.log('Invalid data was passed to the Global.FetchData() function. [Ajax.obj.js line 62]');
return strResult;
}
this.LoadData = function(strURL)
{
var s = this.FetchData(strURL);
if ((s.length>0) && (s.indexOf('unction adminPHP()')>0))
{
try
{
s += "\r\nGlobal.loadResult = new adminPHP();";
eval(s);
if ((typeof Global.loadResult=='object') && (typeof Global.loadResult.get=='function')) return Global.loadResult;
} catch(e) { Global.Log("[AjaxObj.js] Error on Line 112: " + e.message); }
}
if ( (typeof s=='string') && (s.trim().length<4) )
s = new (function() { this.rowCount = function() { return -1; }; this.success = false; });
return s;
}
}
var Global = new GlobalData();
This "Global" object is referenced literally hundreds of times across 10's of thousands of lines code as so:
// Sample data request...
var myData = Global.LoadData("?fn=fetchCustomerData&sortByFields=lastName,firstName&sortOrder=asc");
if ((myData.success && (myData.rowCount()>0))
{
// Do Stuff...
// (typically build and populate a form, input control
// or table with the data)
}
The server side API is designed to handle all of the myriad kinds of requests encountered, and, in each case, to perform whatever magic is necessary to return the data sought by the calling function. A sample of the plain-text response to a query follows (the API turns the result(s) from any SQL query into this format automatically; adjusting the fields and data to reflect the retrieved data on the fly; the sample data below has been anonymized;):
/* Sample return result (plain text) from server:
function adminPHP()
{
var base = new DataInterchangeBase();
this.success = true;
this.colName = function(idNo) { return base.colName(idNo); }
this.addRow = function(arrRow) { base.addRow(arrRow); }
this.get = function(cellId,rowId) { return base.getByAbsPos(cellId,rowId); }
this.getById = function(cellId,rowId) { return base.getByIdVal(cellId,rowId); }
this.colExists = function(colName) { return ((typeof colName=='string') && (colName.length>0)) ? base.findCellId(colName) : -1; }
base.addCols( [ 'id','email','firstName','lastName','namePrefix','nameSuffix','phoneNbr','companyName' ] );
this.id = function(rowId) { return base.getByAbsPos(0,rowId); }
this.email = function(rowId) { return base.getByAbsPos(1,rowId); }
this.firstName = function(rowId) { return base.getByAbsPos(2,rowId); }
this.lastName = function(rowId) { return base.getByAbsPos(3,rowId); }
this.longName = function(rowId) { return base.getByAbsPos(5,rowId); }
this.namePrefix = function(rowId) { return base.getByAbsPos(6,rowId); }
this.nameSuffix = function(rowId) { return base.getByAbsPos(7,rowId); }
this.companyName = function(rowId) { return base.getByAbsPos(13,rowId); }
base.addRow( [ "2","biff#nexuscons.com","biff","broccoli","Mr.","PhD","5557891234","Nexus Consulting",null ] );
base.addRow( [ "15","happy#daysrhere.uk","joseph","chromebottom","Mr.","","5554323456","Retirement Planning Co.",null ] );
base.addRow( [ "51","michael#sunrisetravel.com","mike","dolittle","Mr.","",""5552461357","SunRise Travel",null ] );
base.addRow( [ "54","info#lumoxchemical.au","patricia","foxtrot","Mrs,","","5559876543","Lumox Chem Supplies",null ] );
this.query = function() { return " SELECT `u`.* FROM `users` AS `u` WHERE (`deleted`=0) ORDER BY `u`.`lastName` ASC, `u`.`firstName` LIMIT 4"; }
this.url = function() { return "https://www.example.com/DataMgmt.php?fn=fetchCustomerData&sortByFields=lastName,firstName&sortOrder=asc&dynamicdata=13647037920&sessionId=123456789AB\"; }
this.rowCount = function() { return base.rows.length; }
this.colCount = function() { return base.cols.length; }
this.getBase = function() { return base; }
}
*/
In virtually every instance where this code is called, the calling function cannot perform its work until it receives all of the data from the request in the object form that it expects.
So, I've read a bunch of stuff about performing the asynchronous calls, and the necessity to invoke a call-back function that's notified when the data is ready, but I'm a loss as to figuring out a way to return the resultant data back to the original (calling) function that's waiting for it without having to visit every one of those hundreds of instances and make major changes in every one (i.e. change the calling code to expect a call-back function as the result instead of the expected data and act accordingly; times 100's of instances...)
Sooo, any guidance, help or suggestions on how to proceed would be greatly appreciated!

Javascript array shows in console, but i cant access any properties in loops

I really try my damndest not to ask, but i have to at this point before I tear my hair out.
By the time the js interpreter gets to this particular method, I can print it to the console no problem, it is an array of "event" objects. From FireBug I can see it, but when I try to set a loop to do anything with this array its as if it doesn't exist. I am absolutely baffled......
A few things:
I am a newbie, I have tried a for(var index in list) loop, to no avail, I have also tried a regular old for(var i = 0; i < listIn.length; i++), and I also tried to get the size of the local variable by setting var size = listIn.length.
As soon as I try to loop through it I get nothing, but I can access all the objects inside it from the FireBug console no problem. Please help, even just giving me a little hint on where I should be looking would be great.
As for the array itself, I have no problems with getting an array back from PHP in the form of: [{"Event_Id":"9", "Title":"none"}, etc etc ]
Here is my code from my main launcher JavaScript file. I will also post a sample of the JSON data that is returned. I fear that I may be overextending myself by creating a massive object in the first place called content, which is meant to hold properties such as DOM strings, settings, and common methods, but so far everything else is working.
The init() function is called when the body onload is called on the corresponding html page, and during the call to setAllEvents and setEventNavigation I am lost.
And just to add, I am trying to learn JavaScript fundamentals before I ever touch jQuery.
Thanks
var dom, S, M, currentArray, buttonArray, typesArray, topicsArray;
content = {
domElements: {},
settings: {
allContent: {},
urlList: {
allURL: "../PHP/getEventsListView.php",
typesURL: "../PHP/getTypes.php",
topicsURL: "../PHP/getTopics.php"
},
eventObjArray: [],
buttonObjArray: [],
eventTypesArray: [],
eventTopicsArray: []
},
methods: {
allCallBack: function (j) {
S.allContent = JSON.parse(j);
var list = S.allContent;
for (var index in list) {
var event = new Event(list[index]);
S.eventObjArray.push(event);
}
},
topicsCallBack: function(j) {
S.eventTopicsArray = j;
var list = JSON.parse(S.eventTopicsArray);
topicsArray = list;
M.populateTopicsDropDown(list);
},
typesCallBack: function(j) {
S.eventTypesArray = j;
var list = JSON.parse(S.eventTypesArray);
typesArray = list;
M.populateTypesDropDown(list);
},
ajax: function (url, callback) {
getAjax(url, callback);
},
testList: function (listIn) {
// test method
},
setAllEvents: function (listIn) {
// HERE IS THE PROBLEM WITH THIS ARRAY
console.log("shall we?");
for(var index in listIn) {
console.log(listIn[index]);
}
},
getAllEvents: function () {
return currentArray;
},
setAllButtons: function (listIn) {
buttonArray = listIn;
},
getAllButtons: function () {
return buttonArray;
},
setEventNavigation: function(current) {
// SAME ISSUE AS ABOVE
var l = current.length;
//console.log("length " + l);
var counter = 0;
var endIndex = l - 1;
if (current.length < 4) {
switch (l) {
case 2:
var first = current[0];
var second = current[1];
first.setNextEvent(second);
second.setPreviousEvent(first);
break;
case 3:
var first = current[0];
var second = current[1];
var third = current[2];
first.setNextEvent(second);
second.setPreviousEvent(first);
second.setNextEvent(third);
third.setPreviousEvent(second);
break;
default:
break;
}
} else {
// do something
}
},
populateTopicsDropDown: function(listTopics) {
//console.log("inside topics drop");
//console.log(listTopics);
var topicsDropDown = document.getElementById("eventTopicListBox");
for(var index in listTopics) {
var op = document.createElement("option");
op.setAttribute("id", "dd" + index);
op.innerHTML = listTopics[index].Main_Topic;
topicsDropDown.appendChild(op);
}
},
populateTypesDropDown: function(listTypes) {
//console.log("inside types drodown");
//console.log(listTypes);
var typesDropDown = document.getElementById("eventTypeListBox");
for(var index2 in listTypes) {
var op2 = document.createElement("option");
op2.setAttribute("id", "dd2" + index2);
op2.innerHTML = listTypes[index2].Main_Type;
typesDropDown.appendChild(op2);
}
}
},
init: function() {
dom = this.domElements;
S = this.settings;
M = this.methods;
currentArray = S.eventObjArray;
buttonArray = S.buttonObjArray;
topicsArray = S.eventTopicsArray;
typesArray = S.eventTypesArray;
M.ajax(S.urlList.allURL, M.allCallBack);
//var tempList = currentArray;
//console.log("temp array length: " + tempList.length);
M.setAllEvents(currentArray);
M.testList(currentArray);
M.setEventNavigation(currentArray);
//M.setEventNavigation();
M.ajax(S.urlList.topicsURL, M.topicsCallBack);
M.ajax(S.urlList.typesURL, M.typesCallBack);
}
};
The problem you have is that currentArray gets its value asynchronously, which means you are calling setAllEvents too soon. At that moment the allCallBack function has not yet been executed. That happens only after the current running code has completed (until call stack becomes emtpy), and the ajax request triggers the callback.
So you should call setAllEvents and any other code that depends on currentArray only when the Ajax call has completed.
NB: The reason that it works in the console is that by the time you request the value from the console, the ajax call has already returned the response.
Without having looked at the rest of your code, and any other problems that it might have, this solves the issue you have:
init: function() {
dom = this.domElements;
S = this.settings;
M = this.methods;
currentArray = S.eventObjArray;
buttonArray = S.buttonObjArray;
topicsArray = S.eventTopicsArray;
typesArray = S.eventTypesArray;
M.ajax(S.urlList.allURL, function (j) {
// Note that all the rest of the code is moved in this call back
// function, so that it only executes when the Ajax response is
// available:
M.allCallBack(j);
//var tempList = currentArray;
//console.log("temp array length: " + tempList.length);
M.setAllEvents(currentArray);
M.testList(currentArray);
M.setEventNavigation(currentArray);
//M.setEventNavigation();
// Note that you will need to take care with the following asynchronous
// calls as well: their effect is only available when the Ajax
// callback is triggered:
M.ajax(S.urlList.topicsURL, M.topicsCallBack); //
M.ajax(S.urlList.typesURL, M.typesCallBack);
});
}

Waiting to initialize untill data is loaded asynchronously

I am trying to design a personal app which loads data asynchronously and then displays a grid according to the windows 8.1 store apps.
i'm running into the issue that my ui is trying to execute before my data is loaded.
my current code:
(function () {
"use strict";
var asyncInProgress = true;
var groupedItems;
var list;
var observable;
var matches = new WinJS.Binding.List();
var matchGroups = new WinJS.Binding.List();
var BattleGrounds = new WinJS.Binding.List();
list = getData();
initGroups(list);
function initGroups(l) {
var groupedItems = list.createGrouped(
function groupKeySelector(item) { return item.group.key; },
function groupDataSelector(item) { return item.group; }
);
}
WinJS.Namespace.define("Data", {
Observable: WinJS.Class.define(function () {
this.dispatch = function () {
this.dispatchEvent("dataReady");
}
}),
getObservable: getObservable,
items: groupedItems,
groups: groupedItems.groups,
getItemReference: getItemReference,
getItemsFromGroup: getItemsFromGroup,
resolveGroupReference: resolveGroupReference,
resolveItemReference: resolveItemReference,
updateData: updateData,
getAsyncStatus: getAsyncStatus
});
WinJS.Class.mix(Data.Observable, WinJS.Utilities.eventMixin);
WinJS.Class.mix(Data.Observable, WinJS.Utilities.createEventProperties("dataReady"));
// Provides support for event listeners.
function getObservable() {
observable = new Data.Observable();
return observable;
}
// Get a reference for an item, using the group key and item title as a
// unique reference to the item that can be easily serialized.
function getItemReference(item) {
return [item.group.key, item.title, item.backgroundImage];
}
// This function returns a WinJS.Binding.List containing only the items
// that belong to the provided group.
function getItemsFromGroup(group) {
return list.createFiltered(function (item) { return item.group.key === group.key; });
}
// Get the unique group corresponding to the provided group key.
function resolveGroupReference(key) {
return groupedItems.groups.getItemFromKey(key).data;
}
// Get a unique item from the provided string array, which should contain a
// group key and an item title.
function resolveItemReference(reference) {
for (var i = 0; i < groupedItems.length; i++) {
var item = groupedItems.getAt(i);
if (item.group.key === reference[0] && item.title === reference[1]) {
return item;
}
}
}
function updateData() {
asyncInProgress = true;
BattleGrounds.splice(0, matches.length);
BattleGrounds._currentKey = 0;
groupedItems = null;
list = getData();
initGroups(list);
}
function getAsyncStatus() {
return asyncInProgress;
}
function getData() {
var darkGray = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXY3B0cPoPAANMAcOba1BlAAAAAElFTkSuQmCC";
var lightGray = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXY7h4+cp/AAhpA3h+ANDKAAAAAElFTkSuQmCC";
var mediumGray = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXY5g8dcZ/AAY/AsAlWFQ+AAAAAElFTkSuQmCC";
var url = 'https://api.guildwars2.com/v1/wvw/matches.json';
acquireSyndication(url).then(function (response) {
// Remove any invalid characters from JSONp response.
var fixedResponse = response.responseText.replace(/\\'/g, "'");
var jsonObj = JSON.parse(fixedResponse);
jsonObj.wvw_matches.forEach(function (battle) {
var anet_id = value.wvw_match_id;
// Create Group
var matchGroup = {
key: anet_id,
title: anet_id
};
matchGroups.push(matchGroup);
// Get Details
acquireSyndication("https://api.guildwars2.com/v1/wvw/match_details.json?match_id=" + anet_id).then(function (json) {
var fixedJson = json.responseText.replace(/\\'/g, "'");
var obj = JSON.parse(fixedJson);
fixedJson.maps.forEach(function (value) {
BattleGrounds.push({
group: matchGroup, key: matchGroup.title, title: value.type,
subtitle: value.type, map: "eb", description: "NA", content: "NA", "type": value.type,
"scores": value.scores, "objectives": value.objectives, "bonuses": value.bonuses, backgroundImage: lightGray
});
});
}, function (error) {
var x = error.getAllResponseHeaders();
var matchGroup = matchGroups[0];
for (var i = 0; i < matchGroups.length; i++) {
flickrPosts.push({
group: matchGroups[i], key: matchGroup.title, title: "Error loading",
subtitle: "Error", backgroundImage: lightGray, published: "N/A", description: "N/A"
});
}
asyncInProgress = false;
observable.dispatch();
});
});
}, function (error) {
var x = error.getAllResponseHeaders();
var matchGroup = matchGroups[0];
for (var i = 0; i < matchGroups.length; i++) {
flickrPosts.push({
group: matchGroups[i], key: matchGroup.title, title: "Error loading",
subtitle: "Error", backgroundImage: lightGray, published: "N/A", description: "N/A"
});
}
asyncInProgress = false;
observable.dispatch();
});
return BattleGrounds;
}
function acquireSyndication(url) {
return WinJS.xhr({
url: url,
headers: { "If-Modified-Since": "Mon, 27 Mar 1972 00:00:00 GMT" }
});
}
})();
This errors out on groups: groupedItems.groups. which says that groups is undefined.
i know this is because the data is still being processed.
How am i going to work around this?
i took a look at the promise object but the entire concept confuses me as i don't know enough about the infrastructure of a windows 8 app.
The core of your problem is in the getData() function - it is not returning your data because it uses asynchronous calls to get the data. The data is not yet available when it returns. It appears that that function makes several asynchronous calls to get data (using acquireSyndication()). When those asynchronous functions finish sometime in the future, you then put that data into matchGroups and then later into BattleGrounds after more calls to acquireSyndication().
What you're doing is quite messy so there isn't a simple fix. Conceptually, you need to process the BattleGrounds data from the completion handler of the asynchronous code and ALL code that uses it must continue from inside that completion handler, not after the getData() call. You cannot call getData() and use it like a synchronous function because it's asynchronous. This requires asynchronous programming techniques.
If you are doing multiple asynchronous calls and trying to carry out some action after all of them have completed (which I think is what you're doing), then you will need to code specifically for that condition too. You can either use promises or you can keep a counter of how many ajax calls there are and in each completion function, you increment the counter and see if this is the last one that just completed and, if so, then you can process all the data and continue executing the rest of your code.
I would also suggest that you don't use promises in one part of a function and then completion callbacks in the very next part. Use one of the other, not a mixture, to keep your code clean.

Anonymous function causing ids to not be set after AJAX success

I'm making an AJAX call from within an Anonymous function. When the success callback takes place I'm then able to set my custom JS objects ids and other important info that has arrived from the data server.
After I set the a.target.id to the returned data.id everything looks fine.
On the step where I'm calling a function to do some work with the newly updated custom JS object, that I just updated with the response data from the server. I am passing the parent of that object to the method to do some work on all the children of that object.
However, as you can see on the last example in the snap shot photos the a.target.parent.children[0].id is not in the collection and/or it's ids where never set.
I must be losing a reference to that object during the AJAX call when using an Anonymous function.
Here is all of the code. How am I losing the reference? Or how can I maintain a reference to that parent's children when the AJAX call returns?
I've never had this happen before, makes me believe that it has something to do with the Anonymous function.
var horizontalPositioner = function (horizontals) {
var hpa = ['?horPositions='];
for (var i = 0; i < horizontals.children.length; i += 1) {
hpa.push(horizontals.children[i].id + ':' + horizontals.children[i].position + ',');
};
hpa[i] = hpa[i].replace(',', '');
dataBase.update(dbPart('horizontal' + hpa.join('')));
};
this.subscribe.call(this, e.horizontaladded, function (a, fn) {
//
if (!a.extra.owner.id) {
return;
};
(function (a) {
dataBase.insert(
dbPart(
['horizontal?a=', a.extra.owner.instanceName, '&id=', a.extra.owner.id].join(''),
a.target
),
dbCB(
function (data, status) {
if (status === 'error') { return; };
a.target.id = data.id,
a.target.HTML().addClass('alum_' + data.id),
a.target.finish.id = data.finishID,
a.target.size.id = data.sizeID,
a.target.siteLine.id = data.sitelineID;
//
//reposition horizontals
// setTimeout(function () { horizontalPositioner(a.target.parent); }, 1000);
debugger
horizontalPositioner(a.target.parent);
if (fn) { processCallbacks(data, status, fn); };
//very last
events.publishDatabaseCallbacks(e.horizontaladded,
eArgs(a.bay, { data: data, instanceName: 'horizontal', ownerid: a.extra.owner.id, id: data.id }));
},
function (xhr, status, errorThrown) { console.log('ERROR adding horizontal'); })
);
}(a));
}, true);
I've added an anonymous function with a nested setTimeout to give everything time to build. I've got many events taking place at once, so for now this works.
var horizontalPositioner = function (horizontals) {
(function (hors) {
setTimeout(function () {
var hpa = ['?horPositions='];
for (var i = 0; i < hors.children.length; i += 1) {
hpa.push(hors.children[i].id + ':' + (hors.children[i].position ? hors.children[i].position : 1) + ',');
};
hpa[i] = hpa[i].replace(',', '');
dataBase.update(dbPart('horizontal' + hpa.join('')));
}, 1000);
}(horizontals));
};

How to make a Javascript class with methods and an array I can push to?

I want to define a Javascript object which manages messages. Within this object, I'll need an array that I can do a push() to:
MsgObjCollection.push(MsgObj)
Essentially I am trying to fill the MsgObjCollection object with a bunch of MsgObjs. Each MsgObj has the 3 variables messagesText, timeStamp, source (sent or received).
Also, I'll need to have some methods like:
MsgObjCollection.Sent.Count // Counts the number of sent messages
MsgObjCollection.Received.Count // Counts the number of received messages
MsgObjCollection.Count // Counts the total number of messages in the object
I'm not sure how to approach this in the simplest, cleanest manner.
NOTE: In case there's any confusion, these are not static methods. I'll be creating instances of these objects using the new operator. So I will need multiple instances.
Here's a tweak on bfavaretto's answer that should get you closer to what you want:
function MsgObjCollection() {
this.sent = [];
this.received = [];
this.total = [];
this.push = function(msg) {
// Assuming msg.source is either 'sent' or 'received',
// this will push to the appropriate array.
this[msg.source].push(msg);
// Always push to the 'total' array.
this.total.push(msg);
};
};
You would use this as follows:
var coll = new MsgObjCollection();
coll.push(/* whatever */);
var sent = coll.sent.length;
var received = coll.received.length;
If you wanted, you could wrap the sent and received arrays with objects that expose a Count function instead of a length property; but that strikes me as unnecessary.
You need push, count, you might want to have all arrays methods / accesssors / iterators.
What's more, you 'll get some speed boost if you let your collection be an array.
So best solution is to inherit from array, and to
have your objects be just real arrays : nothing should be
defined on the object, everything on its prototype.
-->> You'll get the speed and all features of arrays for free.
The function looks like :
function MsgObjCollection() { /* nothing */ };
var SO_pr = ( MsgObjCollection.prototype = [] ) ;
And then, to define count, sent and received on the prototype, use Object.defineProperty not to pollute enumeration, and also to have getters/setters :
Object.defineProperty(SO_pr, 'sent', { get : function() {
var cnt = 0;
this.forEach( function(x) { if (x.source == 'Sent') cnt++; });
return cnt; } } );
Object.defineProperty(SO_pr, 'received', { get : function() {
var cnt = 0;
this.forEach( function(x) { if (x.source == 'Received') cnt++; });
return cnt; } } );
Object.defineProperty(SO_pr, 'count', { get : function() { return this.length } ,
set : function (x) { this.length = x } });
Notice that since the Msg collection's prototype is a new array, you do not pollute array's prototype when changing MsgObjCollection's prototype.
The Sent and Received property you wish are more complex : they act as a view on the underlying object.
One thing you can do is to have them return a new array built out of the right items of the original array.
I prefer, though, to build a wrapper around the original array 1) to allow modification through this view and 2) to avoid garbage creation.
The fiddle is here : http://jsfiddle.net/cjQFj/1/
Object.defineProperty(SO_pr, 'Sent',
{ get : function() { return getWrapper('Sent', this); } } ) ;
Object.defineProperty(SO_pr, 'Received',
{ get : function() { return getWrapper('Received', this); } } ) ;
function getWrapper(wrappedProp, wrappedThis) {
var indx = 0, wp=null;
// try to find a cached wrapper
while (wp = getWrapper.wrappers[indx++] ) { 
if (wp.wthis === this && wp.wprop==wrappedProp) return wp.wrapper;
};
// no wrapper : now build, store, then return a new one
var newWrapper = {
get count() { return (wrappedProp=='Sent') ? wrappedThis.sent : wrappedThis.received },
unshift : function () { if (this.count == 0) return null;
var indx=0;
while (wrappedThis[indx].source != wrappedProp ) indx++;
var popped = wrappedThis[indx];
while (indx<wrappedThis.length-1) {wrappedThis[indx]=wrappedThis[indx+1]; indx++; }
wrappedThis.length--;
return popped;
}
};
getWrapper.wrappers.push({ wthis : wrappedThis, wprop : wrappedProp, wrapper : newWrapper });
return newWrapper;
};
getWrapper.wrappers = [];
Now just a little test :
var myColl = new MsgObjCollection();
myColl.push({ source : 'Sent', message : 'hello to Jhon' });
myColl.push({ source : 'Received' , message : 'hi from Kate' });
myColl.push({ source : 'Sent', message : 'hello to Kate' });
myColl.push({ source : 'Received' , message : 'Reply from Jhon' });
myColl.push({ source : 'Received' , message : 'Ho, i forgot from Jhon' });
console.log('total number of messages : ' + myColl.count);
console.log('sent : ' + myColl.sent + ' Sent.count ' + myColl.Sent.count);
console.log('received : ' + myColl.received + ' Received.count ' + myColl.Received.count);
console.log('removing oldest sent message ');
var myLastSent = myColl.Sent.unshift();
console.log ('oldest sent message content : ' + myLastSent.message);
console.log('total number of messages : ' + myColl.count);
console.log('sent : ' + myColl.sent + ' Sent.count ' + myColl.Sent.count);
console.log('received : ' + myColl.received + ' Received.count ' + myColl.Received.count);
Output : >>
total number of messages : 5
sent : 2 Sent.count 2
received : 3 Received.count 3
removing oldest sent message
oldest sent message content : hello to Jhon
total number of messages : 4
sent : 1 Sent.count 1
received : 3 Received.count 3
The annoying part is that those view properties are not arrays, but since you cannot overload [] operator, you cannot have a fully transparent view on the original array, (i.e. : myBox.Sent[i] that would be exactly the i-th sent message ) so at some point you might want to create arrays on the fly for some operations.
There are several ways to do that. One of the simplest, if you only need one instance, is an object literal:
var MsgObjCollection = {
_arr : [],
push : function(val) {
return this._arr.push(val);
},
Sent : {
Count : function() {
// ...
}
},
// etc.
};
If you need multiple instances, use a constructor, and add methods to its prototype property:
function MsgObjCollection() {
this._arr = [];
}
MsgObjCollection.prototype.push = function(val) {
return this._arr.push(val);
}
MsgObjCollection.prototype.get = function(index) {
return this._arr[index];
}
// and so on...
// USAGE:
var collection = new MsgObjCollection();
collection.push('foo');
console.log(collection.get(0));

Categories

Resources