閉包會造成記憶體洩漏嗎?

yancyenough發表於2016-10-29

 前言

  在談記憶體洩漏這個問題之前先看看JavaScript的垃圾收集機制,JavaScript 具有自動垃圾收集機制,就是找出那些不再繼續使用的變數,然後釋放其佔用的記憶體。為此,垃圾收集器會按照固定的時間間隔(或程式碼執行中預定的收集時間)。常用的的方法有兩種,即標記清楚和引用計數。

  標記清除

  JavaScript 中最常用的垃圾收集方式是標記清除(mark-and-sweep)。垃圾收集器在執行的時候會給儲存在記憶體中的所有變數都加上標記(可以使用任何標記方式)。然後,它會去掉環境中的變數以及被環境中的變數引用的變數的標記。而在此之後再被加上標記的變數將被視為準備刪除的變數,原因是環境中的變數已經無法訪問到這些變數了。最後,垃圾收集器完成記憶體清除工作,銷燬那些帶標記的值並回收它們所佔用的記憶體空間。

  引用計數

  引用計數(reference counting)的含義是跟蹤記錄每個值被引用的次數。引用計數的含義是跟蹤記錄每個值被引用的次數。當宣告瞭一個變數並將一個引用型別值賦給該變數時,則這個值的引用次數就是1。如果同一個值又被賦給另一個變數,則該值的引用次數加1。相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數減1。當這個值的引用次數變成0 時,則說明沒有辦法再訪問這個值了,因而就可以將其佔用的記憶體空間回收回來。這樣,當垃圾收集器下次再執行時,它就會釋放那些引用次數為零的值所佔用的記憶體。

  Netscape Navigator 3.0 是最早使用引用計數策略的瀏覽器,但很快它就遇到了一個嚴重的問題,請看下面這個例子:

function problem(){
    var objectA = new Object();
    var objectB = new Object();
    objectA.someOtherObject = objectB;
    objectB.anotherObject = objectA;
}

  說明:objectA 和objectB 通過各自的屬性相互引用,即這兩個物件的引用次數都是2,在採用標記清除策略的實現中,由於函式執行之後,這兩個物件都離開了作用域,因此這種相互引用不是個問題。但在採用引用計數策略的實現中,當函式執行完畢後,objectA 和objectB 還說明將繼續存在,因為它們的引用次數永遠不會是0。假如這個函式被重複多次呼叫,就會導致大量記憶體得不到回收。

  為此,Netscape 在Navigator 4.0 中放棄了引用計數方式,然而引用計數導致的麻煩並未就此了結。IE9以前中有一部分物件並不是原生JavaScript 物件。例如,其BOM 和DOM 中的物件就是使用C++以COM(Component Object Model,元件物件模型)物件的形式實現的,而COM 物件的垃圾收集機制採用的就是引用計數策略。因此,即使IE 的JavaScript 引擎是使用標記清除策略來實現的,但JavaScript 訪問的COM 物件依然是基於引用計數策略的。換句話說,只要在IE 中涉及COM 物件,就會存在迴圈引用的問題。 比如:

var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;

  DOM 元素(element)與一個原生JavaScript 物件(myObject)之間建立了迴圈引用。其中,變數myObject 有一個名為element 的屬性指向element 物件;而變數element 也有一個屬性名叫someObject 回指myObject。由於存在這個迴圈引用,即使將例子中的DOM 從頁面中移除,它也永遠不會被回收。

  解決辦法:將變數設為null從而切斷變數與它此前引用的值之間的連線。

myObject.element = null;
element.someObject = null;

  看完上面的內容,我來談正題。

 閉包不會引起記憶體洩漏

  由於IE9 之前的版本對JScript 物件和COM 物件使用不同的垃圾收集。因此閉包在IE 的這些版本中會導致一些特殊的問題。具體來說,如果閉包的作用域鏈中儲存著一個HTML 元素,那麼就意味著該元素將無法被銷燬
請看例子:

function assignHandler(){
    var element = document.getElementById("someElement");
    element.onclick = function(){
        alert(element.id);
    };
}

  以上程式碼建立了一個作為element 元素事件處理程式的閉包,而這個閉包則又建立了一個迴圈引用(事件將在第13 章討論)。由於匿名函式儲存了一個對assignHandler()的活動物件的引用,因此就會導致無法減少element 的引用數。只要匿名函式存在,element 的引用數至少也是1,因此它所佔用的記憶體就永遠不會被回收

  解決辦法前言已經提到過,把element.id 的一個副本儲存在一個變數中,從而消除閉包中該變數的迴圈引用同時將element變數設為null。

function assignHandler(){
    var element = document.getElementById("someElement");
    var id = element.id;
    element.onclick = function(){
        alert(id);
    };
    element = null;
}

  總結:閉包並不會引起記憶體洩漏,只是由於IE9之前的版本對JScript物件和COM物件使用不同的垃圾收集,從而導致記憶體無法進行回收,這是IE的問題,所以閉包和記憶體洩漏沒半毛錢關係。

相關文章