[译]JavaScript:循环对象检测

原文: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 objectsProxies.(译者注:因为无法添加属性)
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.

posted @ 2012-10-16 14:49  紫云飞  阅读(4801)  评论(1编辑  收藏  举报