-
-
Save getify/3373779 to your computer and use it in GitHub Desktop.
// all this `toJSON()` does is filter out any circular refs. all other values/refs, | |
// it passes through untouched, so it should be totally safe. see the test examples. | |
// only extend the prototype if `toJSON` isn't yet defined | |
if (!Object.prototype.toJSON) { | |
Object.prototype.toJSON = function() { | |
function findCircularRef(obj) { | |
for (var i=0; i<refs.length; i++) { | |
if (refs[i] === obj) return true; | |
} | |
return false; | |
} | |
function traverse(obj) { | |
function element(el) { | |
if (typeof el === "object") { | |
if (el !== null) { | |
if (el instanceof Date || el instanceof Number || el instanceof Boolean || el instanceof String || el instanceof RegExp) { | |
return el; | |
} | |
else if (!findCircularRef(el)) { | |
return traverse(el); | |
} | |
} | |
return null; | |
} | |
return el; | |
} | |
var idx, tmp, tmp2; | |
if (Object.prototype.toString.call(obj) === "[object Array]") { | |
refs.push(obj); | |
tmp = []; | |
for (idx=0; idx<obj.length; idx++) { | |
tmp.push(element(obj[idx])); | |
} | |
refs.pop(); | |
return tmp; | |
} | |
else if (typeof obj === "object") { | |
if (obj !== null) { | |
if (obj instanceof Date || obj instanceof Number || obj instanceof Boolean || obj instanceof String || obj instanceof RegExp) { | |
return obj; | |
} | |
else if (!findCircularRef(obj)) { | |
refs.push(obj); | |
tmp = {}; | |
for (idx in obj) { if (obj.hasOwnProperty(idx)) { | |
tmp2 = element(obj[idx]); | |
if (tmp2 !== null) tmp[idx] = tmp2; | |
}} | |
refs.pop(); | |
return tmp; | |
} | |
} | |
return null; | |
} | |
else return obj; | |
} | |
var refs = [], ret; | |
ret = traverse(this); | |
refs = []; | |
return ret; | |
}; | |
// ES5-only: prevent this `toJSON()` from showing up in for-in loops | |
if (Object.defineProperty) { | |
Object.defineProperty(Object.prototype,"toJSON",{enumerable:false}); | |
} | |
} |
var a = { | |
b: 12, | |
c: true, | |
d: "foobar", | |
e: { | |
f: function() { alert("blah"); }, // functions get ignored | |
g: new Date(), // dates get their own `toJSON()` serialization called | |
h: [ true,1.3,"haha" ] | |
}, | |
k: {}, | |
l: /foobar/g // regexes get turned into an empty {} | |
}; | |
a.i = a; // circular ref!! | |
a.e.i = a; // circular ref!! | |
a.e.j = a.e; // circular ref!! | |
a.e.h.push(a.e); // circular ref!! since it's in array, will be replaced with `null` | |
a.k.m = a.e; // **NOT** a circular ref, just a dupe ref, so leave it alone!! | |
// Look Ma! No circular refs! | |
JSON.stringify(a); // {"b":12,"c":true,"d":"foobar","e":{"g":"2012-08-17T12:11:05.647Z","h":[true,1.3,"haha",null]},"k":{"m":{"g":"2012-08-17T12:11:05.647Z","h":[true,1.3,"haha",null]}},"l":{}} |
Replace findCircularRef(obj) by refs.indexOf(obj)!==false
Ooops
"findCircularRef(obj) by refs.indexOf(obj)>=0"
wrote to quickly
Ooops
"findCircularRef(obj) by refs.indexOf(obj)>0"
wrote to quickly
It would be != -1
... but Array#indexOf
is a "recent edition" to JS, according to: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf Note the compat table which says IE9, meaning my code would break in IE<=8. That's not a good tradeoff to save a few LoC.
I also want to address... why would you want to have a structure with circular refs in it at all, and think that you could JSON serialize it? Is(n't) that just an unreasonable, invalid task?
I don't think it's so unreasonable. I will share two concrete use-cases for the behavior I proposed:
-
I regularly want to do
console.log(JSON.stringify(obj))
during debugging. Why? Well, for one, Firefox doesn't give you any kind of a useful output fromconsole.log(obj)
in their built-in console. And in the webkit/chrome console, that call is "dangerous" in that it's not snapshoting the object at the time of call, but making a live ref to that object, so if you have code that later (after theconsole.log
call) changes the object, those changes frustratingly are reflected in the console.No,
console.dir()
doesn't solve this problem.JSON.stringify()
is a simple way to output a serialization of an object as a snapshot, so you compare two states of an object side-by-side (no,watch
and breakpoint-style debugging doesn't give you that ability).But,
JSON.stringify(obj)
is "unsafe" if the object might have circular refs. It will simply fail with a JSON error. Not very graceful at all.JSON.stringify()
doesn't have a flag you can pass to it to tell it to ignore those circular refs. So you're just sunk. -
It's pretty common that JS apps keep data structures (by way of objects) in the browser, and regularly send all or part of that data over the wire (as JSON) to server to update the model/database persistence layer.
It's also, I've found, sometimes quite helpful to create helpful links between objects that let you jump from one object to another it's related to, and back again, for various UI tasks (like parent-to-child and child-to-parent, etc). These "connections" between objects are just secondary helper refs in the client side... those relationships already exist likely in some other format in the back-end persistence layer, via the RDBMS, etc. So those refs don't need to make the "trip" from client to server; they're only needed on the client.
It is quite useful if I can maintain a "ref-linked" data structure in the client that I can jump around as needed, but then take that same structure and serialize down the data to send over the wire as JSON. This is hugely helpful if the JSON serialization can just ignore all those circular refs.
If this works then we would be able to pass window
or DOM objects to a Web Worker. That would be awesome!
@mohsen1 -- in theory, you could certainly do JSON.stringify(window)
, but I just tried it and it gave me a stack overflow error (because my approach uses recursion). The same would probably be true of a DOM object (if it has a bunch of children, especially).
But, moreover, even if it DID work, there's a bunch of stuff you would lose in the transmission. All the functions built onto the window
object for instance would not be transferred. Variables, properties, and other data would make the trip, yes, but what you'd get on the other side is only a shadow of the original window
... definitely not a copy of it.
ok, circular ref checking is fixed now i think, and the above examples all work.