1. 程式人生 > >簡單聊一聊JS中的迴圈引用及問題

簡單聊一聊JS中的迴圈引用及問題

本文主要從 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(){}) or JSON.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();

執行結果

完,如有不恰當之處,歡迎指正哦