I have this working code, which retrieves the names of object properties from a JS object which (unfortunately!) is out of my scope. So I cannot change how this object is built. But I want to (and do) extract the names of the properties, that are marked as true, as an array, to be able to handle this object easier.
Object:
{
group1: {
foo: true,
itemFoo: "Name of foo", // This is what I want, because foo is true
bar: false,
itemBar: "Name of bar", // I dont want this, bar is false
// ...
},
group2: {
baz: true,
itemBaz: "Name of baz", // I want this too
// ...
},
uselessProp1: "not an object",
// ...
}
Working Code:
var items = [];
for (var m in obj) {
if (typeof obj[m] == 'object') {
for (var n in obj[m]) {
if (obj[m][n] === true) {
items.push(obj[m]['item' + (n.charAt(0).toUpperCase() + n.slice(1))]);
}
}
}
}
My question is: does someone know a more elegant way of achieving this traversal with underscore.js or plain node.js or any other library? I did experiments with _.filter, but did not come up with a solution.
Something like this?
var result = [];
_.chain(obj).filter(_.isObject).each(function(t) {
_(t).each(function(val, key) {
if(val === true)
result.push(t['item' + key.charAt(0).toUpperCase() + key.substr(1)])
})
})
This is the solution I've come so far:
http://jsfiddle.net/kradmiy/28NZP/
var process = function (obj) {
var items = [];
var objectProperties = _(obj).each(function (rootProperty) {
// exit from function in case if property is not an object
if (!_(rootProperty).isObject()) return;
_(rootProperty).each(function (value, key) {
// proceed only if property is exactly true
if (value !== true) return;
var searchedKey = 'item' + (key.charAt(0).toUpperCase() + key.slice(1));
// check that parent has this property...
if (rootProperty.hasOwnProperty(searchedKey)) {
// ...and push that to array
items.push(rootProperty[searchedKey]);
}
});
});
return items;
};
I would like to point out something :
Micha’s Golden Rule
Micha Gorelick, a data scientist in NYC, coined the following rule:
Do not store data in the keys of a JSON blob.
Your JSON should use :
{//group1
groupname:"group1",
items :[
{//item1
itemcheck:true,
itemname:'itemBar'
},
...
]
},
...
If you store itemname in key. You will have problem when traversing the JSON, because your 'itemFoo' would be using 'foo'(indirectly) to get its value. Your data structure, is the problem here. Searching your JSON is tricky. Once you follow the rule, your code will be elegant automatically.
Related
I am trying to delete an item from an object by passing a key to the method. For example I want to delete a1, and to do so I pass a.a1 to the method. It then should delete a1 from the object leaving the rest of the object alone.
This is the structure of the object:
this.record = {
id: '',
expiration: 0,
data: {
a: {
a1: 'Cat'
}
}
}
I then call this method:
delete(key) {
let path = key.split('.')
let data = path.reduce((obj, key) => typeof obj == 'object' ? obj[key] : null, this.record.data)
if(data) delete data
}
Like this:
let inst = new MyClass()
inst.delete('a.a1')
This however gives me the following error:
delete data;
^^^^
SyntaxError: Delete of an unqualified identifier in strict mode.
I assume that data is a reference still at this point, or is it not?
Maybe reduce isn't the right method to use here. How can I delete the item from the object?
Using your example, the value of data at the point where it is checked for truthiness is Cat, the value of the property you're trying to delete. At this point, data is just a regular variable that's referencing a string and it's no longer in the context of inst.
Here's a solution I managed to get to work using the one from your OP as the basis:
let path = key.split('.')
let owningObject = path.slice(0, path.length - 1)
.reduce((obj, key) => typeof obj == 'object' ? obj[key] : null, this.record.data)
if (owningObject) delete owningObject[path[path.length - 1]]
The main difference between this and what you had is that reduce operates on a slice of the path segments, which does not include the final identifier: This ends up with owningObject being a reference to the a object. The reduce is really just navigating along the path up until the penultimate segment, which itself is used as the property name that gets deleted.
For an invalid path, it bails out either because of the if (owningObject) or because using delete on an unknown property is a no-op anyway.
The solution I came up with which I am not super fond of but works, is looping over the items which will allow me to do long keys like this
a.a1
a.a1.a1-1
a.a1.a1-1.sub
The function then looks like this
let record = {
data: {
a: {
a1: 'Cat',
a2: {
val: 'Dog'
}
}
}
}
function remove(key) {
let path = key.split('.')
let obj = record.data
for (let i = 0; i < path.length; i++) {
if (i + 1 == path.length && obj && obj[path[i]]) delete obj[path[i]]
else if(obj && obj[path[i]]) obj = obj[path[i]]
else obj = null
}
}
// Removes `a.a1`
remove('a.a1')
console.log(JSON.stringify(record))
// Removes `a.a2.val`
remove('a.a2.val')
console.log(JSON.stringify(record))
// Removes nothing since the path is invalid
remove('a.a2.val.asdf.fsdf')
console.log(JSON.stringify(record))
You can delete keys using [] references.
var foo = {
a: 1,
b: 2
};
var selector = "a";
delete foo[selector];
console.log(foo);
I'm not sure if this helps you but it might help someone googling to this question.
Here's another method which is very similar to the OP's own solution but uses Array.prototype.forEach to iterate over the path parts. I came to this result independently in my attempt to wrap this up as elegantly as possible.
function TestRecord(id, data) {
let record = {
id : id,
data : data
};
function removeDataProperty(key) {
let parent = record.data;
let parts = key.split('.');
let l = parts.length - 1;
parts.forEach((p, i) => {
if (i < l && parent[p]) parent = parent[p];
else if (i == l && parent[p]) delete parent[p];
else throw new Error('invalid key');
});
}
return {
record : record,
remove : function(key) {
try {
removeDataProperty(key);
} catch (e) {
console.warn(`key ${key} not found`);
}
}
}
}
let test = new TestRecord('TESTA', {
a : { a1 : '1', a2 : '2' },
b : { c : { d : '3' } }
});
test.remove('a'); // root level properties are supported
test.remove('b.c.d'); // deep nested properties are supported
test.remove('a.b.x'); // early exit loop and warn that property not found
console.log(test.record.data);
The usage of throw in this example is for the purpose of breaking out of the loop early if any part of the path is invalid since forEach does not support the break statement.
By the way, there is evidence that forEach is slower than a simple for loop but if the dataset is small enough or the readability vs efficiency tradeoff is acceptable for your use case then this may be a good alternative.
https://hackernoon.com/javascript-performance-test-for-vs-for-each-vs-map-reduce-filter-find-32c1113f19d7
This may not be the most elegant solution but you could achieve the desired result very quickly and easily by using eval().
function TestRecord(id) {
let record = {
id : id,
data : {
a : {
a1 : 'z',
a2 : 'y'
}
}
};
return {
record : record,
remove : function (key) {
if (!key.match(/^(?!.*\.$)(?:[a-z][a-z\d]*\.?)+$/i)) {
console.warn('invalid path');
return;
} else {
let cmd = 'delete this.record.data.' + key;
eval(cmd);
}
}
};
}
let t = new TestRecord('TESTA');
t.remove('a.a1');
console.log(t.record.data);
I have included a regular expression from another answer that validates the user input against the namespace format to prevent abuse/misuse.
By the way, I also used the method name remove instead of delete since delete is a reserved keyword in javascript.
Also, before the anti-eval downvotes start pouring in. From: https://humanwhocodes.com/blog/2013/06/25/eval-isnt-evil-just-misunderstood/ :
...you shouldn’t be afraid to use it when you have a case where eval()
makes sense. Try not using it first, but don’t let anyone scare you
into thinking your code is more fragile or less secure when eval() is
used appropriately.
I'm not promoting eval as the best way to manipulate objects (obviously a well defined object with a good interface would be the proper solution) but for the specific use-case of deleting a nested key from an object by passing a namespaced string as input, I don't think any amount of looping or parsing would be more efficient or succinct.
I'm currently having a problem with a deep search in a json object and even though i thought this issue must have been covered alot, I wasn't able to find anything that was really helpful so far (and I actually found alot, also this thread. Maybe I've been looking at code for too long today but it didn't really help me)
Basically what i want is pretty simple. I have a JSON-Object thats pretty deep filled with objects. All i want is a function that returns an array with all objects that contain a given Key-Value-Pair. I made this function to return the first found object which works just fine
deepSearch: function(Obj, Key, Value){
var returned = [];
var result = false;
var searchObj = function(_Obj, _Key, _Value){
if(_Obj[_Key]===_Value){
return _Obj;
} else {
return false;
}
}
result = searchObj(Obj, Key, Value);
$.each(Obj, function(key, value){
if(typeof(Obj[key]) === 'object' && Obj[key]!== null && !result)
result = customGeneralFunctions.objects.deepSearch(Obj[key], Key, Value);
if(result) return result;
});
return result;
}
Now I want to change it to return an array contianing all Objects with that pair. I've been trying for a while now and I think it wouldnt be a change too hard but I just can't wrap my head around it. Maybesomeone has an idea that helps me. Thanks in advance and
Greetings Chris
A safe deep object search?
Can't let this pass 3 answers with examples, all flawed. And all illustrate some classic Javascript coding got-ya's
null is an Object
UPDATE an answer has been changed.
As the code is no longer visible I will just leave the warning when iterating an object's properties and you use typeof to check if you have an object be careful to check for null as it is also of type "object"
getObject returns to early and fails to find additional objects nested inside objects that meet the condition. Though easily fixed by removing the return it will still throw a TypeError: Cannot read property 'find' of null if the object being searched contains an array with null in it.
for in the indiscriminate iterator
UPDATE an answer has been removed.
I have added the removed code as an example in the snippet below function deepSearch is fatally flawed and will more likely throw a RangeError: Maximum call stack size exceeded error then find the object you are looking for. eg deepSearch({ a:"a"},"id",3);. When using for in you should type check as it will iterate a string as well as an object's properties.
function deepSearch(object, key, value) {
var filtered = [];
for (var p in object)
if (p === key && object[p] === value) filtered.push(object);
else if (object[p]) filtered = filtered.concat(deepSearch(object[p], key, value));
return filtered;
}
Dont trust the callback.
Alex K search passed most tests (within reasonable scope of the question) but only if the code in the form of the comment // tip: here is a good idea to check for hasOwnProperty would have been included.
But that said the function has a flaw (and inefficiency) as it will call predicate on all properties of an object, and I can think of plenty of scenarios in which the function can return many references to the same object eg the reciprocal search for objects with property key NOT with value predicate = (key,val)=>{return key === "id" && val !== 3}.
The search should only add one entry per object thus we should test the object not the properties. We can never trust the callback to do what we expect.
And as it is the accepted answer I should point out that Array.concat should really not be used as it is in this situation. Using closure is much more efficient and allows you to not have to pass the current state to each recursion.
Circular reference.
The flaw to floor them all.
I am not to sure if it is relevant as the question does state that the data is from the form JSON and hence would be free of any circular reference (JSON can not reference).
But I will address the problem and several solutions.
A circular reference is simply an object referencing itself. For example.
var me = {};
me.me = me;
That will crash all the other answers if passed as an argument. Circular references are very common.
Some solutions.
First solution is to only accept data in the form of a JSON string and equally return the data as a JSON string (so balance is maintained and the universe does not explode). Thus eliminating any chance of a circular reference.
Track recursion depth and set a limit. Though this will stop a callstack overflow
it will not prevent the result being flawed as a shallow circular reference can create duplicate object references.
The quick down and dirty solution is a simple try catch around a JSON.stringify and throw TypeError("Object can not be searched"); for those on that side of the data bus..
The best solution is to decycle the object. Which in this case is very amenable to the actual algorithm we are using. For each unique object that is encountered we place it in an array. If we encounter an object that is in that array we ignore it and move on.
A possible solution.
Thus the general purpose solution, that is safe (I hope) and flexible. Though it is written for ES6 so legacy support will have to be provided in the form of babel or the like. Though it does come with a BUT!
// Log function
function log(data){console.log(data)}
// The test data
var a = {
a : "a",
one : {
two : {
find : "me",
data : "and my data in one.two"
},
twoA : {
four : 4,
find : "me",
data : "and my data in one.twoA"
}
},
two : {
one : {
one : 1,
find : "not me",
},
two : {
one : 1,
two : 1,
find : "me",
data : "and my data in two.two"
},
},
anArray : [
null,0,undefined,/./,new Date(),function(){return hi},
{
item : "one",
find : "Not me",
},{
item : "two",
find : "Not me",
extra : {
find : "me",
data : "I am a property of anArray item 1",
more : {
find : "me",
data : "hiding inside me"
},
}
},{
item : "three",
find : "me",
data : "and I am in an array"
},{
item : "four",
find : "me",
data : "and I am in an array"
},
],
three : {
one : {
one : 1,
},
two : {
one : 1,
two : 1,
},
three : {
one : 1,
two : {
one : {
find : "me",
data : "and my data in three.three.two.one"
}
}
}
},
}
// Add cyclic referance
a.extra = {
find : "me",
data : "I am cyclic in nature.",
}
a.extra.cycle = a.extra;
a.extraOne = {
test : [a],
self : a,
findme : a.extra,
};
if(! Object.allWith){
/* Non writeable enumerable configurable property of Object.prototype
as a function in the form
Object.allWith(predicate)
Arguments
predicate Function used to test the child property takes the argument
obj the current object to test
and will return true if the condition is meet
Return
An array of all objects that satisfy the predicate
Example
var test = {a : { key : 10, data: 100}, b : { key : 11, data: 100} };
var res = test.allWith((obj)=>obj.key === 10);
// res contains test.a
*/
Object.defineProperty(Object.prototype, 'allWith', {
writable : false,
enumerable : false,
configurable : false,
value : function (predicate) {
var uObjects = [];
var objects = [];
if (typeof predicate !== "function") {throw new TypeError("predicate is not a function")}
(function find (obj) {
var key;
if (predicate(obj) === true) {objects.push(obj)}
for (key of Object.keys(obj)) {
let o = obj[key];
if (o && typeof o === "object") {
if (! uObjects.find(obj => obj === o)) {
uObjects.push(o);
find(o);
}
}
}
} (this));
return objects;
}
});
}else{
console.warn("Warn!! Object.allWith already defined.");
}
var res = a.allWith(obj => obj.find === "me");
res.forEach((a,i)=>(log("Item : " + i + " ------------"),log(a)))
Why are you searching through unknown data structures?
It works for all the test cases I could come up with, but that is not at all the definitive test. I added it to the Object.prototype because you should not do that!!! nor use such a function or derivative thereof.
This is the first time I have written such a function, and the reason is that I have never had to write something like that before, I know what the data looks like and I dont have to create dangerous recursive iterators to find what is needed.. If you are writing code and you are not sure of the data you are using there is something wrong in the design of the whole project.
Hopefully this will help you to solve your task.
Lets use recursion to search deep into object.
Also lets make it more generic.
// search function takes object as a first param and
// a predicate Function as second predicate(key, value) => boolean
function search(obj, predicate) {
let result = [];
for(let p in obj) { // iterate on every property
// tip: here is a good idea to check for hasOwnProperty
if (typeof(obj[p]) == 'object') { // if its object - lets search inside it
result = result.concat(search(obj[p], predicate));
} else if (predicate(p, obj[p]))
result.push(
obj
); // check condition
}
return result;
}
Lets test it!
var obj = {
id: 1,
title: 'hello world',
child: {
id: 2,
title: 'foobar',
child: {
id: 3,
title: 'i should be in results array '
}
},
anotherInnerObj: {
id: 3,
title: 'i should be in results array too!'
}
};
var result = search(obj, function(key, value) { // im looking for this key value pair
return key === 'id' && value === 3;
});
Output:
result.forEach(r => console.log(r))
// Object {id: 3, title: "i should be in results array "}
// Object {id: 3, title: "i should be in results array too!"}
You've created a returned array. First, push the result of searchObj() into it. Then in your loop, if you get a result, concat() it to returned. Finally, return returned at the end of the function. That should do it...
You could use a simplified version and
check if object not truthy or object is not an object, then return
check if given key and value match, then add the actual object to the result set,
get the keys and iterate over the properties and call the function again.
At last, the array with the collected objects is returned.
function getObjects(object, key, value) {
function iter(o) {
if (!o || typeof o !== 'object') {
return;
}
if (o[key] === value){
result.push(o);
}
Object.keys(o).forEach(function (k) {
iter(o[k]);
});
}
var result = [];
iter(object);
return result;
}
var object = { id: 1, title: 'hello world', child: { id: null, title: 'foobar', child: { id: null, title: 'i should be in results array ' } }, foo: { id: null, title: 'i should be in results array too!' }, deep: [{ id: null, value: 'yo' }, { id: null, value: 'yo2' }] };
console.log(getObjects(object, 'id', null));
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have an object in javaScript:
var stuffObject = {
stuffArray1 : [object1, object2, object3],
stuffArray2 : [object4, object5, object6]
}
object1 to 6 look like this:
object1 = {
dataStuff : {
stuffId: "foobar"
}
}
My question: given the key "foobar", how do I retrieve object1 from the stuffObject using jQuery? The key "stuffId" always has a unique value.
You won't get around iterating over the set to find the object you are looking for. jQuery can't really help with that. Its purpose is DOM manipulation. If you want functionality to deal with objects, sets, lists, etc., check out lodash.
I wrote a function to deal with the problem. I hope it's understandable.
var stuffObject = {
stuffArray1 : [{dataStuff: {stuffId: 'foobar'}}, {dataStuff: {stuffId: 'foo'}}, {}],
stuffArray2 : [{}, {dataStuff: {stuffId: 'bar'}}, {}]
}
function getObjByStuffId(stuffObject, stuffId) {
var key, arr, i, obj;
// Iterate over all the arrays in the object
for(key in stuffObject) {
if(stuffObject.hasOwnProperty(key)) {
arr = stuffObject[key];
// Iterate over all the values in the array
for(i = 0; i < arr.length; i++) {
obj = arr[i];
// And if it has the value we are looking for
if(typeof obj.dataStuff === 'object'
&& obj.dataStuff.stuffId === stuffId) {
// Stop searching and return the object.
return obj;
}
}
}
}
}
console.log('foobar?', getObjByStuffId(stuffObject, 'foobar') );
console.log('foo?', getObjByStuffId(stuffObject, 'foo') );
console.log('bar?', getObjByStuffId(stuffObject, 'bar') );
Thanks for the help guys, using the input of other people I have solved it myself:
getStuffById: function(id){
for (stuffArray in stuffObject) {
for (stuff in stuffObject[stuffArray]) {
if (stuffObject[stuffArray][stuff].dataStuff.stuffId == id) {
return stuffObject[stuffArray][stuff];
}
}
}
return null;
}
This also works better than the (now deleted) answer that uses .grep(), as this function terminates as soon as it finds the correct object.
I am currently breaking my head about transforming this object hash:
"food": {
"healthy": {
"fruits": ['apples', 'bananas', 'oranges'],
"vegetables": ['salad', 'onions']
},
"unhealthy": {
"fastFood": ['burgers', 'chicken', 'pizza']
}
}
to something like this:
food:healthy:fruits:apples
food:healthy:fruits:bananas
food:healthy:fruits:oranges
food:healthy:vegetables:salad
food:healthy:vegetables:onions
food:unhealthy:fastFood:burgers
food:unhealthy:fastFood:chicken
food:unhealthy:fastFood:pizza
In theory it actually is just looping through the object while keeping track of the path and the end result.
Unfortunately I do not know how I could loop down till I have done all nested.
var path;
var pointer;
function loop(obj) {
for (var propertyName in obj) {
path = propertyName;
pointer = obj[propertyName];
if (pointer typeof === 'object') {
loop(pointer);
} else {
break;
}
}
};
function parse(object) {
var collection = [];
};
There are two issues which play each out:
If I use recurse programming it looses the state of the properties which are already parsed.
If I do not use it I cannot parse infinite.
Is there some idea how to handle this?
Regards
The reason your recursive function doesn't work is you're storing the state outside it. You want the state inside it, so that each invocation tracks its state.
Something like this:
var obj = /* ... the object ... */;
var lines = loop([], "", obj);
function loop(lines, prefix, obj) {
var key, sawOne = false;
// Is it an array?
if (Object.prototype.toString.call(obj) === "[object Array]") {
// Yes, in your example these are all just strings to put
// at the end, so do that
for (key = 0; key < obj.length; ++key) {
lines.push(prefix + ":" + obj[key]);
}
}
else {
// No, it's an object. Recurse for each property, adding the
// property to the prefix we use on each line
for (key in obj) {
loop(lines, prefix ? (prefix + ":" + key) : key, obj[key]);
}
}
return lines;
}
Completely off-the-cuff and untested, but you get the idea.
Edit: But apparently it works, as Michael Jasper was kind enough to make a live demo (source) which I've tweaked slightly.
What's the quickest and easiest way to convert my json, containing the data of the objects, into actual objects with methods attached?
By way of example, I get data for a fruitbowl with an array of fruit objects which in turn contain an array of seeds thus:
{"fruitbowl": [{
"name": "apple",
"color": "red",
"seeds": []
},{
"name": "orange",
"color": "orange",
"seeds": [
{"size":"small","density":"hard"},
{"size":"small","density":"soft"}
]}
}
That's all nice and good but down on the client we do stuff with this fruit, like eat it and plant trees...
var fruitbowl = []
function Fruit(name, color, seeds){
this.name = name
this.color = color
this.seeds = seeds
this.eat = function(){
// munch munch
}
}
function Seed(size, density){
this.size = size
this.density = density
this.plant = function(){
// grow grow
}
}
My ajax's success routine currently is currently looping over the thing and constructing each object in turn and it doesn't handle the seeds yet, because before I go looping over seed constructors I'm thinking
Is there not a better way?
success: function(data){
fruitbowl.length = 0
$.each(data.fruitbowl, function(i, f){
fruitbowl.push(new Fruit(f.name, f.color, f.seeds))
})
I haven't explored looping over the objects as they are and attaching all the methods. Would that work?
Yes, it would work, but it's not desirable. Apart from appearing slightly hacky IMO, you're attaching methods to each instance of your fruit and seeds, where you should instead be using the prototype chain. If you're going to be using instanceof in the future, this method won't work anyway.
What you're currently doing is the best solution; and you'll be able to use instanceof.
If you're feeling adventurous, you can use JSONP instead of AJAX, with the JSONP response looking something like:
buildFruitbowl([new Fruit("orange", "blue", [new Seed("small", "hard"), new Seed("big", "soft")]), new Fruit("banana", "yellow", [new Seed("small", "hard"), new Seed("big", "soft")])]);
Which will save you having to do all your object looping, and you'll get your Fruit and Seeds how you want (and instanceof support); however I would still stick to what you're doing already.
Best of look growing your bananas.
Pass the data to the object constructor then use jquery's "extend" to combine the data and methods:
function Fruit(data){
$.extend(this, data)
this.eat = function(){
// munch munch
}
}
...
$.each(data.fruitbowl, function(i, f){
fruitbowl.push(new Fruit(f))
})
You still have loops involved; and must manually code loops for the nested objects (like seeds), but still a very simple way to get past the problem.
You could modify the JSON structure to store the type information. If you have a lot of objects to serialize and deserialize back and forth, this would save time writing custom code for each object.
Also note, this modifies the JSON structure and adds a __type__ property to each custom object. I think this is a cleaner approach than keeping separate configuration files. So without further ado, this is how it basically works:
var fruitBowl = {..};
fruitBowl[0].eat();
fruitBowl[1].seeds[0].plant();
call serialize on the object to get a JSON representation
var json = fruitBowl.serialize();
call deserialize on the JSON encoded string to reconstruct the objects
var resurrected = json.deserialize();
now you can access properties and call methods on the objects:
resurrected[0].eat();
resurrected[1].seeds[0].plant();
It works for any levels of deeply nested objects, although it might be a little buggy for now. Also it is most likely not cross-browser (only tested on Chrome). Since the deserializer is not familiar with an object's constructor function, it basically creates each custom object without passing any parameters. I've setup a working demo on jsfiddle at http://jsfiddle.net/kSATj/1/.
The constructor function had to be modified to account for the two ways it's objects could be created
Directly in Javascript
Reconstructed from JSON
All constructors would need to accommodate creation from both ends, so each property needs to be assigned a default fallback value incase nothing was passed.
function SomeObject(a, b) {
this.a = a || false; // defaultValue can be anything
this.b = b || null; // defaultValue can be anything
}
// one type of initialization that you can use in your code
var o = new SomeObject("hello", "world");
// another type of initialization used by the deserializer
var o = new SomeObject();;
o.a = "hello";
o.b = "world";
For reference, the modified JSON looks like:
{"fruitbowl":
[
{
"__type__": "Fruit",
"name": "apple",
"color": "red",
"seeds": []
},
{
"__type__": "Fruit",
"name": "orange",
"color": "orange",
"seeds":
[
{
"__type__": "Seed",
"size": "small",
"density": "hard"
},
{
"__type__": "Seed",
"size": "small",
"density": "soft"
}
]
}
]
}
This is just a helper function to identify simple types:
function isNative(object) {
if(object == null) {
return true;
}
var natives = [Boolean, Date, Number, String, Object, Function];
return natives.indexOf(object.constructor) !== -1;
}
Serializes an object into JSON (with type info preserved):
Object.prototype.serialize = function() {
var injectTypes = function(object) {
if(!isNative(object)) {
object.__type__ = object.constructor.name;
}
for(key in object) {
var property = object[key];
if(object.hasOwnProperty(key) && !isNative(property)) {
injectTypes(property);
}
}
};
var removeTypes = function(object) {
if(object.__type) {
delete object.__type__;
}
for(key in object) {
var property = object[key];
if(object.hasOwnProperty(key) && !isNative(property)) {
removeTypes(property);
}
}
}
injectTypes(this);
var json = JSON.stringify(this);
removeTypes(this);
return json;
};
Deserialize (with custom objects reconstructed):
String.prototype.deserialize = function() {
var rawObject = JSON.parse(this.toString());
var reconstruct = function(object) {
var reconstructed = {};
if(object.__type__) {
reconstructed = new window[object.__type__]();
delete object.__type__;
}
else if(isNative(object)) {
return object;
}
for(key in object) {
var property = object[key];
if(object.hasOwnProperty(key)) {
reconstructed[key] = reconstruct(property);
}
}
return reconstructed;
}
return reconstruct(rawObject);
};
Using ES5 Object.create
Simply define your objects statically then use Object.create to extend them.
It's as simple as Object.create(Bowl, transform(data));
// declare 3 Objects to use as prototypes for your data
var Fruit = {
eat: function() { }
}
var Seed = {
plant: function() { }
}
var Bowl = {};
// data object
var data = { ... };
// Transform JSON to a valid defineProperties hash.
Object.create(Bowl, transform(data));
You will need to define the transform function and more importantly tell it the object type of nested arrays of data.
// hash map of property names of arrays to the Object they should prototype from.
var collectionClassHash = {
fruitbowl: Fruit,
seeds: Seed
}
var transform = function(obj) {
// return value
var ret = {};
// for each key
Object.keys(obj).forEach(function(key) {
// value of key
var temp = obj[key];
// if array
if (Array.isArray(temp) {
// override value with an array of the correct objects
temp = obj[key].map(function(val) {
// recurse for nested objects
return Object.create(collectionClassHash[key], transform(val));
});
}
// define getter/setter for value
ret[key] = {
get: function() { return temp; },
set: function(v) { temp = v; }
}
});
return ret;
}
Using D Crockford's "json2" library, you can supply a "reviver" function to the parsing process. The reviver function is passed each key and each value, and should return the actual effective value to be used in the parsed result.
There's a corresponding optional parameter in the "stringify" method.
This actually took me a while to figure out, I'm really surprised there are not more pages on this.
As #Pointy pointed out, JSON has a reviver function that can be used to replace the parse result inline allowing you to avoid walking the tree a second time. The JSON page documents reviver (in my opinion a little weakly) - http://json.org/js.html.
Reviver is part of ECMA 5 and is supported in Firefox, WebKit (Opera/Chrome), and JSON2.js.
Here is a code example based on the JSON doc. You can see we are setting a type property on Dog and then using a reviver function that recognizes that type property.
function Dog(args) {
this.name = args.name;
this.bark = function() {
return "bark, bark, my name is " + this.name;
};
this.toJSON = function() {
return {
name: this.name,
type: 'Dog' // this.constructor.name will work in certain browsers/cases
}
}
};
var d = new Dog({name:'geti'});
var dAsJson = JSON.stringify(d);
var dFromJson = JSON.parse(dAsJson, function (key, value) {
var type;
if (value && typeof value === 'object') {
type = value.type;
if (typeof type === 'string' && typeof window[type] === 'function') {
return new (window[type])(value);
}
}
return value;
}
);
I have a couple concerns about their example. The first is that it depends on the constructor being global (on window). The second is a security concern in that rogue JSON can get us to call any constructor by adding a type property to their JSON.
I've chosen to have an explicit list of types and their constructors. This ensures only constructors I know are safe will be called and also allows me to use a custom type mapping approach if I like (rather than depending on the constructor name and it being in the global space). I also verify the JSON object has a type (some may not and they will be treated normally).
var jsonReviverTypes = {
Dog: Dog
};
var dAsJsonB = JSON.stringify(d);
var dFromJsonB = JSON.parse(dAsJsonB, function (key, value) {
var type;
if (value && typeof value === 'object' && value.type) {
type = value.type;
if (typeof type === 'string' && jsonReviverTypes[type]) {
return new (jsonReviverTypes[type])(value);
}
}
return value;
});
Note, FF 3.6 has a bug in the JSON.replacer method as #Sky pointed out and has documented here - http://skysanders.net/subtext/archive/2010/02/24/confirmed-bug-in-firefox-3.6-native-json-implementation.aspx. For the above solution I work around this by using toJSON on the object rather than using replacer.
John,
Hopefully not too late to chip in here. I had a very similar problem just last week and solved it with the following piece of js (it could easily be converted to jquery as well.).
Here's the base usage:
$(document).ready(function() {
var bowl = { "fruitbowl": [{
"name": "apple",
"color": "red",
"seeds": []
},
{
"name": "orange",
"color": "orange",
"seeds": [
{ "size": "small", "density": "hard" },
{ "size": "small", "density": "soft"}]
}
]
};
var serialized = jsonToObject.serialize(bowl);
var deserialized = jsonToObject.deserialize(serialized);
// basic tests on serialize/deserializing...
alert(deserialized.fruitbowl[0].name);
alert(deserialized.fruitbowl[1].seeds[0].density);
});
and here's the jsonToObject.js file:
jsonToObject = {
deserialize: function(_obj) {
if (typeof (JSON) === 'object' && typeof (JSON.parse) === 'function') {
// native JSON parsing is available.
//return JSON.parse(_obj);
}
// otherwise, try non-native methods
var jsonValue = new Function("return " + _obj)();
if (!jsonValue instanceof Object) {
jsonValue = eval("(" + _obj + ")");
}
return jsonValue;
},
serialize: function(_obj) {
// Let Gecko browsers do this the easy way - not working
if (_obj != undefined && typeof _obj.toSource !== 'undefined'
&& typeof _obj.callee === 'undefined') {
return _obj.toSource();
}
// Other browsers must do it the hard way
switch (typeof _obj) {
// numbers, booleans, and functions are trivial:
// just return the object itself since its default .toString()
// gives us exactly what we want
case 'number':
case 'boolean':
case 'function':
return _obj;
break;
// for JSON format, strings need to be wrapped in quotes
case 'string':
return '"' + _obj.replace(/"/mg, "'") + '"';
break;
case 'object':
var str;
if (_obj.constructor === Array || typeof _obj.callee !== 'undefined') {
str = '[';
var i, len = _obj.length;
for (i = 0; i < len - 1; i++) { str += this.serialize(_obj[i]) + ','; }
str += this.serialize(_obj[i]) + ']';
}
else {
str = '{';
var key;
for (key in _obj) { str += key + ':' + this.serialize(_obj[key]) + ','; }
str = str.replace(/\,$/, '') + '}';
}
return str;
break;
default:
return '""';
break;
}
}
}
hope this helps...
jim
[edit] - you could of course also give the two functions their prototype signatures in keeping with the excellent example above, ie..
String.prototype.deserialize = function() {...}
Object.prototype.serialize = function() {...}