閉包拾遺 & 垃圾回收機制

閉包拾遺 

  以前寫了篇《閉包初窺》,談了一些我對閉包的淺顯認識,在前文基礎上,補充而且更新些對於閉包的認識。html

  仍是以前的那個經典的例子,來補充些經典的解釋。瀏覽器

function outerFn() {
  var a = 0;
  function innerFn() {
    console.log(a++);
  }
  return innerFn;
}

var fn = outerFn();
fn(); // 0
fn(); // 1

  這裏並無在outerFn內部修改全局變量,而是從outerFn中返回了一個對innerFn的引用。經過調用outerFn可以得到這個引用,並且這個引用能夠能夠保存在變量中。 這種即便離開函數做用域的狀況下仍然可以經過引用調用內部函數的事實,意味着只要存在調用內部函數的可能,JavaScript就須要保留被引用的函數。並且JavaScript運行時須要跟蹤引用這個內部函數的全部變量,直到最後一個變量廢棄,JavaScript的垃圾收集器才能釋放相應的內存空間。閉包

  讓咱們說的更透徹一些。所謂「閉包」,就是在構造函數體內定義另外的函數做爲目標對象的方法函數,而這個對象的方法函數反過來引用外層函數體中的臨時變量。這使得只要目標對象在生存期內始終能保持其方法,就能間接保持原構造函數體當時用到的臨時變量值。儘管最開始的構造函數調用已經結束,臨時變量的名稱也都消失了,但在目標對象的方法內卻始終能引用到該變量的值,並且該值只能通這種方法來訪問。即便再次調用相同的構造函數,但只會生成新對象和方法,新的臨時變量只是對應新的值,和上次那次調用的是各自獨立的。函數

  仍是前文的例子:spa

<ul>
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
</ul>
<script>
  var lis = document.getElementsByTagName('li');
  for(var i = 0; i < lis.length; i++) {
    ~function(num) {
      lis[i].onclick = function() {
        alert(num)
      };
    }(i)
  }
</script>

  爲何不加當即執行函數,alert的都會是5呢?指針

  若是不加IIFE,當i的值爲5的時候,判斷條件不成立,for循環執行完畢,可是由於每一個li的onclick方法這時候爲內部函數,因此i被閉包引用,內存不能被銷燬,i的值會一直保持5,直到程序改變它或者全部的onclick函數銷燬(主動把函數賦爲null或者頁面卸載)時纔會被回收。這樣每次咱們點擊li的時候,onclick函數會查找i的值(做用域鏈是引用方式),一查等於5,而後就alert給咱們了。加上IIFE後便是又建立了一層閉包,函數聲明放在括號內就變成了表達式,後面再加上括號就是調用了,這時候把i當參數傳入,函數當即執行,num保存每次i的值。code

垃圾回收機制(GC)

  接下來講說垃圾回收機制(Garbage Collecation)。htm

  在上面的第一個例子中,變量始終保存在內存中,說到底與JavaScript的垃圾回收機制有關。JavaScript垃圾回收的機制很簡單:找出再也不使用的變量,而後釋放掉其佔用的內存,可是這個過程不是實時的,由於其開銷比較大,因此垃圾回收器會按照固定的時間間隔週期性的執行。再也不使用的變量也就是生命週期結束的變量,固然只多是局部變量,全局變量的生命週期直至瀏覽器卸載頁面纔會結束。局部變量只在函數的執行過程當中存在,而在這個過程當中會爲局部變量在棧或堆上分配相應的空間,以存儲它們的值,而後在函數中使用這些變量,直至函數結束,而閉包中因爲內部函數的緣由,外部函數並不能算是結束。對象

  仍是上代碼說明吧:blog

function fn1() {
  var obj = {name: 'hanzichi', age: 10};
}

function fn2() {
  var obj = {name:'hanzichi', age: 10};
  return obj;
}

var a = fn1();
var b = fn2();

  咱們來看代碼是如何執行的。首先定義了兩個function,分別叫作fn1和fn2,當fn1被調用時,進入fn1的環境,會開闢一塊內存存放對象{name: 'hanzichi', age: 10},而當調用結束後,出了fn1的環境,那麼該塊內存會被js引擎中的垃圾回收器自動釋放;在fn2被調用的過程當中,返回的對象被全局變量b所指向,因此該塊內存並不會被釋放。

垃圾回收機制的種類

  函數中的局部變量的生命週期:局部變量只在函數執行的過程當中存在。而在這個過程當中,會爲局部變量在棧(或堆)內存上分配相應的空間,以便存儲它們的值。而後在函數中使用這些變量,直至函數執行結束。此時,局部變量就沒有存在的必要了,所以能夠釋放它們的內存以供未來使用。在這種狀況下,很容易判斷變量是否還有存在的必要;但並不是全部狀況下都這麼容易就能得出結論。垃圾回收器必須跟蹤哪一個變量有用,哪一個變量沒用,對於再也不有用的變量打上標記,以備未來收回其佔用的內存。用於標識無用變量的策略可能會因實現而異,但具體到瀏覽器中的實現,則一般有兩個策略。

  • 標記清除

  js中最經常使用的垃圾回收方式就是標記清除。當變量進入環境時,例如,在函數中聲明一個變量,就將這個變量標記爲「進入環境」。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,由於只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記爲「離開環境」。

  垃圾回收器在運行的時候會給存儲在內存中的全部變量都加上標記(固然,可使用任何標記方式)。而後,它會去掉環境中的變量以及被環境中的變量引用的變量的標記(閉包)。而在此以後再被加上標記的變量將被視爲準備刪除的變量,緣由是環境中的變量已經沒法訪問到這些變量了。最後,垃圾回收器完成內存清除工做,銷燬那些帶標記的值並回收它們所佔用的內存空間。

  到2008年爲止,IE、Firefox、Opera、Chrome、Safari的js實現使用的都是標記清除的垃圾回收策略或相似的策略,只不過垃圾收集的時間間隔互不相同。

  • 引用計數  

  引用計數的含義是跟蹤記錄每一個值被引用的次數。當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是1。若是同一個值又被賦給另外一個變量,則該值的引用次數加1。相反,若是包含對這個值引用的變量又取得了另一個值,則這個值的引用次數減1。當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,於是就能夠將其佔用的內存空間回收回來。這樣,當垃圾回收器下次再運行時,它就會釋放那些引用次數爲0的值所佔用的內存。

  Netscape Navigator3是最先使用引用計數策略的瀏覽器,但很快它就遇到一個嚴重的問題:循環引用。循環引用指的是對象A中包含一個指向對象B的指針,而對象B中也包含一個指向對象A的引用。

function fn() {
  var a = {};
  var b = {};
  a.pro = b;
  b.pro = a;
}

fn();

  以上代碼a和b的引用次數都是2,fn()執行完畢後,兩個對象都已經離開環境,在標記清除方式下是沒有問題的,可是在引用計數策略下,由於a和b的引用次數不爲0,因此不會被垃圾回收器回收內存,若是fn函數被大量調用,就會形成內存泄露

  咱們知道,IE中有一部分對象並非原生js對象。例如,其DOM和BOM中的對象就是使用C++以COM對象的形式實現的,而COM對象的垃圾回收機制採用的就是引用計數策略。所以,即便IE的js引擎採用標記清除策略來實現,但js訪問的COM對象依然是基於引用計數策略的。換句話說,只要在IE中涉及COM對象,就會存在循環引用的問題。

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

  這個例子在一個DOM元素(element)與一個原生js對象(myObject)之間建立了循環引用。其中,變量myObject有一個名爲element的屬性指向element對象;而變量element也有一個屬性名爲o回指myObject。因爲存在這個循環引用,即便例子中的DOM從頁面中移除,它也永遠不會被回收。

  爲了不相似這樣的循環引用問題,最好是在不使用它們的時候手工斷開原生js對象與DOM元素之間的鏈接:

myObject.element = null;
element.o = null;

  將變量設置爲null意味着切斷變量與它此前引用的值之間的鏈接。當垃圾回收器下次運行時,就會刪除這些值並回收它們佔用的內存。

相關文章
相關標籤/搜索