原文:http://blog.vjeux.com/2011/javascript/cyclic-object-detection.html
包含循环结构的对象称之为循环对象,循环对象无法遍历,因为在遍历过程中会产生死循环.本文讲了三种用来检测一个对象是否循环对象的技术.
译者注:创建循环对象
作者没有讲怎么创建一个循环对象,我觉的有必要讲一下.循环对象是一个自身的某个属性指向自己的对象.可以这样来创建.
var foo = {}; foo["bar"] = foo; jQuery.param(foo); //这是一个死循环,浏览器报错InternalError: too much recursion
包含一个循环对象的对象也是循环对象
var obj = {key:foo} jQuery.param(obj); //InternalError: too much recursion
还有一种Mozilla的私有技术可以创建循环对象,叫井号变量,不过从Firefox12起,已经废弃
var foo = #1= {bar: #1#} jQuery.param(a); //InternalError: too much recursion
你肯定用过循环对象,因为:
window.window.window.window.window.window.window === window
给对象的每个属性加标记
想要检测一个对象内部是否包含了循环结构,最先想到的方法就是给每个节点添加标记.在遍历过程中,如果我们遇到一个已经被标记过的节点,也就说明该对象包含了循环结构.
这种方法修改了一个不属于我们的对象.这是很危险的,会有很多其他的影响.
- 使用什么来作为唯一的不与现有属性重复的标记键?我用了
Math.random,但是这样仍然有可能和对象已有的属性重复,不仅会导致错误的判断结果,还会让该属性被误删除
! - 添加一个新的属性,然后再删除它,这样会反复改变对象的内存占用,很可能导致内存拷贝(memory copy)以及内存空洞(memory holes).
- 该方法不能处理Sealed objects 和 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; }
把标记存储在另外一个独立的数据结构中
显然,我们应该避免编辑原对象,但该怎么避免呢?我想到的办法是使用一个数组把访问过的节点存储下来.然后使用indexOf方法
,判断我们是否访问过这个节点,可以,这是一个O(n²)的复杂度,而上面的方法是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); }
underscore.js使用了这种方法来检测循环对象.
利用原生的JSON.stringify
最后一种方法有点技巧性.如果浏览器支持ES5中的JSON对象.JSON.stringify会在处理循环对象时抛出异常.
function isCyclic(obj) { var isNativeJSON = typeof JSON !== 'undefined' && JSON.stringify.toString().match(/\n\s*\[native code\]\s*\n/); if (!isNativeJSON) { throw 'Native JSON.stringify is not available, can\'t use this technique.'; } try { JSON.stringify(obj); return false; } catch (e) { return true; } }
总结
得出的结论有点让人沮丧,因为没有一种技术能完全满足我们.为了能够写出一种完美的进行循环对象检测的代码,我们需要一种哈希表结构的对象,但目前还没有(译者注:我们现在有Map了).
如果你感兴趣的话,上面的这些源代码都存储在github上,另外还有用来比较这三种技术性能的jsperf.
如果你需要存储或加载某个循环结构,可以使用Douglas Crockford的 decycle & retrocycle functions.