原文: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.