本文主要從 JS 中為什么會出現循環引用,垃圾回收策略中引用計數為什么有很大的問題,以及循環引用時的對象在使用 JSON.stringify 時為什么會報錯,怎樣解決這個問題簡單談談自己的一些理解。
1. 什么是循環引用
當對象 1 中的某個屬性指向對象 2,對象 2 中的某個屬性指向對象 1 就會出現循環引用,(當然不止這一種情況,不過原理是一樣的)下面通過代碼和內存示意圖來說明一下。
function circularReference() {
let obj1 = {
};
let obj2 = {
b: obj1
};
obj1.a = obj2;
}
上述代碼在內存中的示意圖
從上圖可以看出 obj1 中的 a 屬性引用 obj2,obj2 中的 b 屬性引用 obj1,這樣就構成了循環引用。
2.JS 中引用計數垃圾回收策略的問題
先簡單講一下 JS 中引用垃圾回收策略大體是什么樣的一個原理,當一個變量被賦予一個引用類型的值時,這個引用類型的值的引用計數加 1。就像是代碼中的 obj1 這個變量被賦予了 obj1 這個對象的地址,obj1 這個變量就指向了這個 obj1(右上)這個對象,obj1(右上)的引用計數就會加1.當變量 obj1的值不再是 obj1(右上)這個對象的地址時,obj1(右上)這個對象的引用計數就會減1.當這個 obj1(右上)對象的引用計數變成 0 后,垃圾收集器就會將其回收,因為此時沒有變量指向你,也就沒辦法使用你了。
看似很合理的垃圾回收策略為什么會有問題呢?
就是上面講到的循環引用導致的,下面來分析一下。當 obj1 這個變量執行 obj1 這個對象時,obj1 這個對象的引用計數會加 1,此時引用計數值為 1,接下來 obj2 的 b 屬性又指向了 obj1 這個對象,所以此時 obj1 這個對象的引用計數為 2。同理 obj2 這個對象的引用計數也為2.
當代碼執行完后,會將變量 obj1 和 obj2 賦值為 null,但是此時 obj1 和 obj2 這兩個對象的引用計數都為1,並不為 0,所以並不會進行垃圾回收,但是這兩個對象已經沒有作用了,在函數外部也不可能使用到它們,所以這就造成了內存泄露。
在現在廣泛采用的標記清除回收策略中就不會出現上面的問題,標記清除回收策略的大致流程是這樣的,最開始的時候將所有的變量加上標記,當執行 cycularReference 函數的時候會將函數內部的變量這些標記清除,在函數執行完后再加上標記。這些被清除標記又被加上標記的變量就被視為將要刪除的變量,原因是這些函數中的變量已經無法被訪問到了。像上述代碼中的 obj1 和 obj2 這兩個變量在剛開始時有標記,進入函數后被清除標記,然后函數執行完后又被加上標記被視為將要清除的變量,因此不會出現引用計數中出現的問題,因為標記清除並不會關心引用的次數是多少。
3. 循環引用的對象使用 JSON.stringify 為什么會報錯
JSON.stringify
用於將一個 JS 對象序列化為一個 JSON 字符串,假設現在我們要將 obj1 這個對象序列化為 JSON 字符串,現在我們先將 obj1 這個對象打印出來看一下。
function circularReference() {
let obj1 = {
};
let obj2 = {
b: obj1
};
obj1.a = obj2;
console.log(obj1);
}
circularReference();
結果如下所示:
obj1 這個對象和 obj2 會無限相互引用,JSON.tostringify 無法將一個無限引用的對象序列化為 JOSN 字符串。
下面是 MDN 的解釋:
JSON.stringify() 將值轉換為相應的JSON格式:
- 轉換值如果有 toJSON() 方法,該方法定義什么值將被序列化。
- 非數組對象的屬性不能保證以特定的順序出現在序列化后的字符串中。
- 布爾值、數字、字符串的包裝對象在序列化過程中會自動轉換成對應的原始值。
undefined
、任意的函數以及 symbol 值,在序列化過程中會被忽略(出現在非數組對象的屬性值中時)或者被轉換成null
(出現在數組中時)。函數、undefined 被單獨轉換時,會返回 undefined,如JSON.stringify(function(){})
orJSON.stringify(undefined)
.- 對包含循環引用的對象(對象之間相互引用,形成無限循環)執行此方法,會拋出錯誤。
- 所有以 symbol 為屬性鍵的屬性都會被完全忽略掉,即便
replacer
參數中強制指定包含了它們。- Date 日期調用了 toJSON() 將其轉換為了 string 字符串(同Date.toISOString()),因此會被當做字符串處理。
- NaN 和 Infinity 格式的數值及 null 都會被當做 null。
- 其他類型的對象,包括 Map/Set/WeakMap/WeakSet,僅會序列化可枚舉的屬性。
我們可以從加粗的字體中看到,對包含循環引用的對象執行 JSON.stringify,會拋出錯誤。
解決方法
一個自然的想法能不能消除循環引用,一個 JSON 擴展包 做到了這一點, 使用 JSON.decycle 可以去除循環引用。為了方便測試我直接在 JSON 擴展包的 Github 倉庫中下載了 cycle.js 這個函數,將下面這段代碼賦值到最下面,然后利用 node 運行進行測試,問題得到解決,結果如下圖所示。
function circularReference() {
let obj1 = {
};
let obj2 = {
b: obj1
};
obj1.a = obj2;
let c = JSON.decycle(obj1);
console.log(JSON.stringify(c));
}
circularReference();
運行結果
完,如有不恰當之處,歡迎指正哦。