URLON.stringify() suffer from a problem, when passed an object that contains a cycle, it will never stop. This article shows 3 techniques in order to detect if an object is cyclical.
Edit the object: Mark
In order to detect a cycle in an object, the method we learn at school is to mark each visited node. When doing a traversal, if we encounter an already marked node, then it is cyclic.
This method alters an object we don't own. It is a dangerous practice and has many side effects.
- How do we find a unique mark key? I chose to use
Math.random
but it has a slight chance of giving the wrong result and removing this key from the object! - Adding a new key then deleting it makes the object memory size change. Therefore there are great chances of introducing memory copy and at the end of the process, memory holes because of the attribute deletion.
- It does not work with Sealed objects and Proxies.
function isCyclic (obj) { var seenObjects = []; var mark = String(Math.random()); function detect (obj) { if (typeof obj === 'object') { if (mark in obj) { return false; } obj[mark] = true; seenObjects.push(obj); for (var key in obj) { if (obj.hasOwnProperty(key) && !detect(obj[key])) { return false; } } } return true; } var result = detect(obj); for (var i = 0; i < seenObjects.length; ++i) { delete seenObjects[i][mark]; } return result; } |
Keep the mark in a separate data structure
Obviously, we want to avoid editing the object, but what solutions do we have? The only one I found is keeping the visited nodes in an array. Then using indexOf
, we can detect if we already visited the node.
However, this is a O(n²) process where the previous one was O(n).
function isCyclic (obj) { var seenObjects = []; function detect (obj) { if (typeof obj === 'object') { if (seenObjects.indexOf(obj) !== -1) { return true; } seenObjects.push(obj); for (var key in obj) { if (obj.hasOwnProperty(key) && detect(obj[key])) { return true; } } } return false; } return detect(obj); } |
Use native JSON.stringify
The last technique is a bit hacky. When implemented by the browser, JSON.stringify will throw an error if the object happens to be cyclic. It requires a recent browser and feels wrong.
function isCyclic (obj) { var isNativeJSON = typeof JSON !== 'undefined' && JSON.stringify.toString().match(/\{ \[native code\] \}$/); if (!isNativeJSON) { throw 'Native JSON.stringify is not available, can\'t use this technique.'; } try { JSON.stringify(obj); return true; } catch (e) { return false; } } |
Conclusion
The conclusion is a bit sad, none of the techniques above are satisfying. In order to make a pure-Javascript and efficient version of cycle detection, it would require an Object hash-map, which we don't currently have.
If you are interested, the code source is on github and there's a jsperf that compares the 3 techniques.
Related Work
If you need to store and load circular structures, use Douglas Crockford decycle & retrocycle functions.