1. 程式人生 > >JavaScript中4種常見的記憶體洩漏及避免方法

JavaScript中4種常見的記憶體洩漏及避免方法

垃圾回收演算法


       常用垃圾回收演算法叫做**標記清除 (Mark-and-sweep) **,演算法由以下幾步組成:

  • 1、垃圾回收器建立了一個“roots”列表。roots 通常是程式碼中全域性變數的引用。JavaScript 中,“window” 物件是一個全域性變數,被當作 root 。window 物件總是存在,因此垃圾回收器可以檢查它和它的所有子物件是否存在(即不是垃圾);

  • 2、所有的 roots 被檢查和標記為啟用(即不是垃圾)。所有的子物件也被遞迴地檢查。從 root 開始的所有物件如果是可達的,它就不被當作垃圾。

  • 3、所有未被標記的記憶體會被當做垃圾,收集器現在可以釋放記憶體,歸還給作業系統了。

       現代的垃圾回收器改良了演算法,但是本質是相同的:可達記憶體被標記,其餘的被當作垃圾回收。

 

四種常見的JS記憶體洩漏


1、意外的全域性變數

未定義的變數會在全域性物件建立一個新變數,如下:

function foo(arg) {
    bar = "this is a hidden global variable";
}

函式 foo 內部忘記使用 var ,實際上JS會把bar掛載到全域性物件上,意外建立一個全域性變數:

function foo(arg) {
    window.bar = "this is an explicit global variable";
}

另一個意外的全域性變數可能由 this 建立:

function foo() {
    this.variable = "potential accidental global";
}

// Foo 呼叫自己,this 指向了全域性物件(window)
// 而不是 undefined
foo();

解決方法

       在 JavaScript 檔案頭部加上 'use strict'

,使用嚴格模式避免意外的全域性變數,此時上例中的this指向undefined。如果必須使用全域性變數儲存大量資料時,確保用完以後把它設定為 null 或者重新定義。

 

2、被遺忘的計時器或回撥函式

計時器setInterval程式碼很常見:

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 處理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

上面的例子表明,在節點node或者資料不再需要時,定時器依舊指向這些資料。所以哪怕當node節點被移除後,interval 仍舊存活並且垃圾回收器沒辦法回收,它的依賴也沒辦法被回收,除非終止定時器。

var element = document.getElementById('button');
function onClick(event) {
    element.innerHTML = 'text';
}

element.addEventListener('click', onClick);

對於上面觀察者的例子,一旦它們不再需要(或者關聯的物件變成不可達),明確地移除它們非常重要。老的 IE 6 是無法處理迴圈引用的。因為老版本的 IE 是無法檢測 DOM 節點與 JavaScript 程式碼之間的迴圈引用,會導致記憶體洩漏。

但是,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收演算法(標記清除),已經可以正確檢測和處理迴圈引用了。即回收節點記憶體時,不必非要呼叫 removeEventListener 了。

 

3、脫離 DOM 的引用

如果把DOM 存成字典(JSON 鍵值對)或者陣列,此時,同樣的 DOM 元素存在兩個引用:一個在 DOM 樹中,另一個在字典中。那麼將來需要把兩個引用都清除。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
    // 更多邏輯
}
function removeButton() {
    // 按鈕是 body 的後代元素
    document.body.removeChild(document.getElementById('button'));
    // 此時,仍舊存在一個全域性的 #button 的引用
    // elements 字典。button 元素仍舊在記憶體中,不能被 GC 回收。
}

如果程式碼中儲存了表格某一個 <td> 的引用。將來決定刪除整個表格的時候,直覺認為 GC 會回收除了已儲存的 <td> 以外的其它節點。實際情況並非如此:此 <td> 是表格的子節點,子元素與父元素是引用關係。由於程式碼保留了 <td> 的引用,導致整個表格仍待在記憶體中。所以儲存 DOM 元素引用的時候,要小心謹慎。

 

4、閉包

閉包的關鍵是匿名函式可以訪問父級作用域的變數:

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };

  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};

setInterval(replaceThing, 1000);

每次呼叫 replaceThing ,theThing 得到一個包含一個大陣列和一個新閉包(someMethod)的新物件。同時,變數 unused 是一個引用 originalThing 的閉包(先前的 replaceThing 又呼叫了 theThing )。someMethod 可以通過 theThing 使用,someMethod 與 unused 分享閉包作用域,儘管 unused 從未使用,它引用的 originalThing 迫使它保留在記憶體中(防止被回收)。

解決方法是,在 replaceThing 的最後新增 originalThing = null 。