But I frequently come across the situation where at least one of these objects is refreshed from the server (eg. in async comet event) and this relationship is lost. A lot of case-by-case code is required to be written to support such instances on the client side.
Today I've been thinking about tracking the object references generally in JavaScript and how it can be done. Unfortunately it looks that it can't. JavaScript doesn't really support real references like eg. in C language, that could be used to achieve this. In JS function there's no way to have access to reference that carries the object as the function argument, and thus to modify this reference.
But indeed there's a way to have access to the reference - using the closure. Closure holds references to all objects belonging to the closure scope. After a little time of playing I figured out some solution for such hypothetical reference tracker:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Creates new reference manager | |
* @constructor | |
*/ | |
function Reference() { | |
function RefHolder() { | |
this.assignFunc = []; | |
} | |
/** | |
* Creates or updated the reference | |
* @param {object} value Value to be assigned | |
* @param {function(value)|object} funcOrObject Assignment function, assigning value to some variable | |
* accessible from closure or object when we are changing the assignment from one reference to another | |
*/ | |
this.assign = function(value, funcOrObject) { | |
if (typeof funcOrObject == 'function') { | |
// this is the first assignment | |
// assign value to variable using func | |
funcOrObject(value); | |
// create value $ref to hold all references | |
if (!value.$ref) | |
value.$ref = new RefHolder(); | |
value.$ref.assignFunc.push(funcOrObject); | |
} else { | |
// this is the reassignment | |
if (funcOrObject.$ref) { | |
for (var i=0; i<funcOrObject.$ref.assignFunc.length; i++) { | |
funcOrObject.$ref.assignFunc[i](value); | |
} | |
value.$ref = funcOrObject.$ref; | |
} | |
} | |
}; | |
} | |
/******************************************************************** | |
* Test code | |
********************************************************************/ | |
function createJson(value) { | |
return { | |
id: 1, | |
value: 'root_'+value, | |
items: [ | |
{ | |
id: 2, | |
value: 'item2_'+value | |
}, | |
{ | |
id: 3, | |
value: 'item3_'+value | |
}, | |
{ | |
id: 4, | |
value: 'item4_'+value | |
} | |
] | |
} | |
} | |
var ref = new Reference(); | |
// create two variables referencing to each other | |
var root1, root2; | |
ref.assign(createJson('A'), function(value) {root1 = value}); // assigns JSON A to root1 | |
ref.assign(root1, function(value) {root2 = value}); // assigns root1 to root2 | |
console.log('root1 after assignment', root1); // root_A | |
console.log('root2 after assignment', root2); // root_A | |
// modify single variable to see reflected changes for both | |
ref.assign(createJson('B'), root1); // changes the root1 assignment to JSON B - reflected in root2 assignment | |
console.log('root1 after reassignment', root1); // root_B | |
console.log('root2 after reassignment', root2); // root_B |
Now, we have some more complex case. We are just re-assigning single object holding whole list, and we may have only the item (from the example above) assigned elsewhere. This is not as simple as the previous example, but feasible if we think about the model as persistent objects model. All persistent objects have some unique ID assigned. For example if you use UUID on server side, it can be UUID. If you use the numeric ID, it can be combination of ID and object type (class) etc. You can always figure out easily some other method to have unique ID for you objects some way.
In such instance, to achieve the goal we need to have the previous JSON structure, and to scan new one, looking for objects with the same ID, and then reuse our "reference tracker" above. Here is full source including the new usage:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Creates new reference manager | |
* @param {function(object)} idFunc returning string representing unique ID of object | |
* @constructor | |
*/ | |
function Reference(idFunc) { | |
this.idFunc = idFunc; | |
function RefHolder(id) { | |
this.id = id; | |
this.assignFunc = []; | |
} | |
/** | |
* Creates or updated the reference | |
* @param {object} value Value to be assigned | |
* @param {function(value)|object} funcOrObject Assignment function, assigning value to some variable | |
* accessible from closure or object when we are changing the assignment from one reference to another | |
*/ | |
this.assign = function(value, funcOrObject) { | |
if (typeof funcOrObject == 'function') { | |
// this is the first assignment | |
// assign value to variable using func | |
funcOrObject(value); | |
// create value $ref to hold all references | |
if (!value.$ref) | |
value.$ref = new RefHolder(this.idFunc ? this.idFunc(value) : null); | |
value.$ref.assignFunc.push(funcOrObject); | |
} else { | |
// this is the reassignment | |
if (funcOrObject.$ref) { | |
for (var i=0; i<funcOrObject.$ref.assignFunc.length; i++) { | |
funcOrObject.$ref.assignFunc[i](value); | |
} | |
value.$ref = funcOrObject.$ref; | |
} | |
} | |
}; | |
/** | |
* Scans object recursively to look for all objects | |
* @param {object} object Object to scan | |
* @param {object[]} array All objects contained 'object' | |
*/ | |
function scanR(object, array) { | |
array.push(object); | |
for (var prop in object) | |
if (prop!='$ref' && typeof object[prop] == 'object') { | |
if (object[prop] instanceof Array) { | |
for (var i=0; i<object[prop].length; i++) | |
scanR(object[prop][i], array); | |
} else | |
scanR(object[prop], array); | |
} | |
} | |
/** | |
* Reassigns object recursively. The object should be previously assigned and a proper idFunc should | |
* be given for this Reference. | |
* @param {object} value New reference | |
* @param {object} object Previous reference | |
*/ | |
this.assignR = function(value, object) { | |
if (!idFunc) | |
throw new Exception('This Reference has no idFunc defined'); | |
var oldObjects = []; | |
var newObjects = []; | |
scanR(object, oldObjects); | |
scanR(value, newObjects); | |
// lookup associated refs | |
var oldRefs = {}; | |
var newRefs = {}; | |
for (var i=0; i<oldObjects.length; i++) | |
if (oldObjects[i].$ref) | |
oldRefs[oldObjects[i].$ref.id] = oldObjects[i]; | |
for (i=0; i<newObjects.length; i++) | |
if (idFunc(newObjects[i])) | |
newRefs[idFunc(newObjects[i])] = newObjects[i]; | |
// reassign | |
for (var id in oldRefs) | |
if (newRefs[id]) | |
this.assign(newRefs[id], oldRefs[id]); | |
}; | |
/** | |
* Cleans up the reference variable from $ref for JSON serialization. | |
* @param object | |
*/ | |
this.cleanup = function(object) { | |
var clean = {}; | |
for (var prop in object) { | |
if (prop!='$ref') | |
clean[prop] = object[prop]; | |
} | |
return clean; | |
} | |
} | |
/******************************************************************** | |
* Test code | |
********************************************************************/ | |
function createJson(value) { | |
return { | |
id: 1, | |
value: 'root_'+value, | |
items: [ | |
{ | |
id: 2, | |
value: 'item2_'+value | |
}, | |
{ | |
id: 3, | |
value: 'item3_'+value | |
}, | |
{ | |
id: 4, | |
value: 'item4_'+value | |
} | |
] | |
} | |
} | |
var ref = new Reference(function(object) { | |
return object.id; | |
}); | |
// create two variables referencing to each other | |
var root1, root2; | |
ref.assign(createJson('A'), function(value) {root1 = value}); // assigns JSON to root1 | |
ref.assign(root1, function(value) {root2 = value}); // assigns root1 to root2 | |
console.log('root1 after assignment', root1); // root_A | |
console.log('root2 after assignment', root2); // root_A | |
// modify single variable to see reflected changes for both | |
ref.assign(createJson('B'), root1); // changes the root1 assignment - reflected in root2 assignment | |
console.log('root1 after reassignment', root1); // root_B | |
console.log('root2 after reassignment', root2); // root_B | |
// now we will test something complex - recursive assignment | |
var item; | |
ref.assign(createJson('C'), function(value) {root1 = value}); // assigns JSON to root1 | |
ref.assign(root1.items[0], function(value) {item = value}); // assigns items[0] to item | |
console.log('root1 after assignment', root1); // root_C | |
console.log('item after assignment', item); // item_C | |
ref.assignR(createJson('D'), root1); // assigns new JSON to root1, results in root1 and item | |
// assignment due to proper idFunc | |
console.log('root1 after reassignmentR', root1); // root_D | |
console.log('item after reassignmentR', item); // item_D |