四種常見的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 或者重新定義。

被遺忘的計時器或回調函數

計時器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 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章