1. 程式人生 > 實用技巧 >呼叫堆疊(五)-記憶體洩漏

呼叫堆疊(五)-記憶體洩漏

四種常見的JS記憶體洩漏

1、意外的全域性變數

JavaScript處理未定義變數的方式比較寬鬆:未定義的變數會在全域性物件建立一個新變數。在瀏覽器中,全域性物件是window。

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

真相是:

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

函式foo內部忘記使用var,意外建立了一個全域性變數。

另外一種意外可能是this建立:

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

在 JavaScript 檔案頭部加上 'use strict',可以避免此類錯誤發生。啟用嚴格模式解析 JavaScript ,避免意外的全域性變數。

2、被遺忘的計時器或回撥函式
在JavaScript中使用setInterval非常平常。一段常見的程式碼:

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

此例說明了什麼:與節點或資料關聯的計時器不再需要,node 物件可以刪除,整個回撥函式也不需要了。可是,計時器回撥函式仍然沒被回收(計時器停止才會被回收)。同時,someResource 如果儲存了大量的資料,也是無法被回收的。

對於觀察者的例子,一旦它們不再需要(或者關聯的物件變成不可達),明確地移除它們非常重要。老的 IE 6 是無法處理迴圈引用的。如今,即使沒有明確移除它們,一旦觀察者物件變成不可達,大部分瀏覽器是可以回收觀察者處理函式的。

觀察者程式碼示例:

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

老版本的 IE 是無法檢測 DOM 節點與 JavaScript 程式碼之間的迴圈引用,會導致記憶體洩漏。如今,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收演算法,已經可以正確檢測和處理迴圈引用了。換言之,回收節點記憶體時,不必非要呼叫 removeEventListener 了。

3、脫離DOM的引用
有時,儲存 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 回收。
  }

4、閉包

 function fn () {
      var str = "aaaaa";
      return function () {
          console.log(str);
      };
  }

變數a被fn()函式內的匿名函式所引用,因此這種變數是不會被回收的。

記錄一下一開始自己的問題:

  <body>
      <div id="div">fasdf</div>
    <script>
        var list = []
        var divs = document.getElementsByTagName("div")

        list.push(divs[0])
        document.body.removeChild(divs[0])

        console.log(divs)
        console.dir(list)
    </script>
  </body>

這裡我一開始認為陣列和變數是對同一個物件的引用,所以他倆應該都為空。
很明顯這是錯誤的。
首先我們應該把div這個標籤元素當做物件存在記憶體中,dom節點物件引用了它,所以在頁面中才顯示了出來。當我們通過let a = document.getElementById('div')獲取的時候,獲取到的直接是對記憶體中這個物件的引用。然後進行remove刪除操作的時候,是刪掉了dom節點物件對它的引用,而不會影響a對它的引用。
我們上面的程式碼var divs = document.getElementsByTagName("div"),獲取的divs是對dom節點物件的引用,而這個dom節點物件裡又有對div標籤元素的引用,當我們push操作時push的是對記憶體中div標籤元素的引用,所以remove之後,dom節點物件失去了對它的引用,結果顯示HTMLCollection[], 而陣列list並不為空。

參考:
https://jinlong.github.io/2016/05/01/4-Types-of-Memory-Leaks-in-JavaScript-and-How-to-Get-Rid-Of-Them/