深刻理解閉包的概念

閉包

關於閉包,目前有以下說法:html

  • 閉包是函數和聲明該函數的詞法環境的組合(MDN)
  • 函數對象能夠經過做用域鏈相互關聯起來,函數體內部的變量均可以保存在函數做用域內。這種特性在計算機科學文獻中被稱爲閉包(JavaScript權威指南)
  • 閉包,指的是詞法表示包括不被計算的變量的函數,也就是說,函數可使用函數以外定義的變量(W3school)
  • 閉包是指有權訪問另外一個函數做用域中的變量的函數(JavaScript高級程序設計)

根據排列順序也能夠看出,我我的對這些說法的認同程度。其實你們說的都是同一個東西,只是描述是否精確的問題。
爲了充分理解以上的說法,要先理解一些術語:前端


詞法做用域

簡單來講,詞法做用域就是:根據變量定義時所處的位置,來肯定變量的做用範圍。(詞法解析,經過閱讀包含變量定義在內的數行源碼就能知道變量的做用域)
舉例而言,定義在全局的變量,它的做用範圍是全局的,因此被稱爲全局變量;定義在函數內部的變量,它的做用範圍是局部的,因此被稱爲局部變量。chrome

做用域鏈

函數在建立時,會同時保存它的做用域鏈。——這個保存的做用域鏈包含了該函數所處的做用域對象的集合。由於全部函數都在全局做用域下聲明,因此這個保存的做用域鏈必定包含全局做用域對象(global)。此外,若是函數是在其餘函數內部聲明的,那它保存的做用域鏈中除了global以外,還包含它建立時所處的局部做用域對象。(在chrome中直接標識爲closure,在firefox中則標識爲塊)。顯然,這個做用域鏈其實是一個指向做用域對象集合的指針列表瀏覽器

函數在執行時,會建立一個執行環境、執行時做用域鏈以及活動對象。——活動對象(activation object)是指當前做用域對象(處於活動狀態的,它包含arguments、this以及全部局部變量)。執行時做用域鏈其實是函數建立時保存的做用域鏈的一個複製,但它更長,由於活動對象被推入了執行時做用域鏈的前端。每次函數在執行時都會建立一個新的執行環境(execution context),它對應着一個全新的執行時做用域鏈。閉包

根據JavaScript的垃圾回收機制:通常狀況下,函數在執行完畢後,執行環境(包括執行時做用域鏈)將自動被銷燬,佔用的內存將被釋放。函數

垃圾回收機制

JavaScript 是一門具備自動垃圾回收機制的語言。
這種機制的原理是找出那些再也不繼續使用的變量,而後釋放其佔用的內存。目前,找出再也不繼續使用的變量的策略有兩種:標記清除(主流瀏覽器)和引用計數(IE8及如下)。
標記清除:垃圾收集器在運行的時候會給存儲在內存中的全部變量都加上標記;而後,它會去掉環境中的變量以及被環境中的變量引用的變量的標記;最後,垃圾收集器銷燬那些帶標記的值並回收它們所佔用的內存空間。垃圾收集器會按照固定的時間間隔週期性地執行這一操做。
引用計數:當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是 1。若是同一個值又被賦給另外一個變量,則該值的引用次數加 1。相反,若是包含對這個值引用的變量又取得了另一個值,則這個值的引用次數減 1。當這個值的引用次數變成 0 時,則說明沒有辦法再訪問這個值了,於是就能夠將其佔用的內存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數爲零的值所佔用的內存。(引用計數的失敗之處在於它沒法處理循環引用)性能


如今,什麼是閉包呢?
——「閉包是函數和聲明該函數的詞法環境的組合」(MDN)this

function a(){
  console.log('1');
}
a();

以上例子:函數a,和它建立時所在的全局做用域,構成一個閉包。因而有人說每一個函數實際上都是一個閉包,但準確來說,應該是每一個函數和它建立時所處的做用域構成一個閉包。
但這個閉包叫什麼名字呢?
在chrome和firefox調試中,將函數a所在做用域的名字,做爲閉包的名字;在JavaScript高級程序設計中則將函數a的名字,做爲閉包的名字。這樣一來,每一個函數都是一個閉包的說法彷佛又「準確」了一些。
其實咱們書寫的全部js代碼,都處在全局做用域這個大大的閉包之中,只是咱們意識不到它做爲一個閉包存在着。spa

function a(){
  var b = 1;
  function c(){
    console.log(b);
  }
  return c
}
var d = a();
d(); // 1

以上例子:除了函數a和全局做用域構成一個閉包之外,函數c和局部做用域(函數a的做用域)也構成一個閉包。
先不關注這些函數內部的邏輯,咱們只看結構:
函數a聲明瞭,而後在var d = a();這一句執行。經過以上對詞法做用域、做用域鏈以及垃圾回收機制的理解,咱們能夠得出如下結論:
函數a在聲明時保存了一個做用域鏈,在它執行時又建立了一個執行環境(以及執行時做用域鏈)。通常狀況下,當函數a執行完畢,它的執行環境將被銷燬。但在這個例子裏,函數a中的變量c,被return突破做用域的限制賦值給了變量d,而變量c是一個函數,它使用了它建立時所處的做用域(函數a的做用域)中的變量b,這意味着,在函數d執行完畢以前,函數c以及它建立時所處的做用域中變量(變量b)不能夠被銷燬。
這打斷了函數a執行環境的銷燬進程,它被保存了下來,以備函數d調用時使用。看看被保存的是什麼?一個函數c和它建立時所在的做用域。一個閉包。firefox

function a(){
  var b = 1;
  function c(){
    b++; console.log(b);
  }
  return c
}
var d = a();
d(); // 2
d(); // 3
var e = a();
e(); // 2
e(); // 3

以上例子,函數a被執行了兩次並分別賦值給了d、e,顯然,函數a的兩次執行建立了兩個執行環境,它們本該被銷燬,但因爲函數c的存在(有權訪問另外一個函數內部變量的函數),它們被保存下來。函數d的兩次執行,使用同一個執行環境中的變量b,因此b遞增了;因爲函數e使用的是另外一個執行環境中的變量b,因此它從新開始遞增。

因此,什麼是閉包呢?
閉包是一個函數和它建立時所在做用域的組合。在咱們平常應用中,一般是將一個函數定義在另外一個函數的內部並從中返回,以使它成爲一個在函數外部仍有權限訪問函數內部做用域的函數。
jQuery就是定義在一個匿名自執行函數內部的函數,當它被賦值給全局做用域變量$jQuery時,在全局做用域使用$jQuery方法,就可以訪問到那個匿名自執行函數的內部做用域(其中包含的變量等)。在jQuery這個例子中,內部函數jQuery和其所在的匿名自執行函數做用域就構成一個閉包。

一個經典的例子:

// html <ul><li></li><li></li><li></li></ul>
var lis = document.querySelector('ul').children;
for (var i = 0; i < lis.length; i++) {
  lis[i].addEventListener('click', function(){
    console.log(i);
  })
}
var event = document.createEvent('MouseEvent');
event.initEvent('click', false, false);
for (var j = 0; j < lis.length; j++) {
  lis[j].dispatchEvent(event);
}

爲頁面上的全部li標籤綁定點擊函數,點擊後輸出自身的序號。在以上例子中,顯然將輸出 3, 3, 3;而非 0, 1, 2;
一個通俗的解釋是,當點擊li標籤時,for循環已經執行完畢,i的值已經肯定。因此三個li標籤點擊輸出同一個i的值。
咱們稍微改動一下代碼:

// html <ul><li></li><li></li><li></li></ul>
var lis = document.querySelector('ul').children;
for (var i = 0; i < lis.length; i++) {
  (function(i){
    lis[i].addEventListener('click', function(){
      console.log(i);
    })
  })(i);
}
var event = document.createEvent('MouseEvent');
event.initEvent('click', false, false);
for (var j = 0; j < lis.length; j++) {
  lis[j].dispatchEvent(event);
}

以上例子,當點擊li標籤時,for循環已經執行完畢,i的值已經肯定,可爲何結果會輸出 0, 1, 2 呢?
實際上,這是閉包在做怪:
  click事件的匿名函數 跟外層自執行匿名函數的做用域構成了一個閉包。在循環中,外層匿名自執行函數本該在執行結束後銷燬它的執行環境,釋放其內存,但因爲它的參數(變量)i 還被事件監聽函數引用着,因此這個執行環境沒法被銷燬,它將被保存着。每一次的循環,匿名自執行函數都將執行一次,並保存一個執行環境;當循環結束,相似的執行環境共有三個,每個裏面的變量i的值都是不一樣的。
  回到第一個例子,匿名事件函數實際上和聲明它的全局做用域也構成了一個閉包,但在三次循環中,i 都不曾離開這個閉包,它一直遞增直至3,三個點擊事件函數引用同一個執行環境中的變量i,它們的值必然是相同的。

離開閉包的泥淖,給這個例子一個較爲合理的寫法:

// html <ul><li></li><li></li><li></li></ul>
var lis = document.querySelector('ul').children;
var say = function(){
  console.log(this.index);
}
for (var i = 0; i < lis.length; i++) {
  lis[i].index = i;
  lis[i].addEventListener('click', say);
}

var event = document.createEvent('MouseEvent');
event.initEvent('click', false, false);
for (var j = 0; j < lis.length; j++) {
  lis[j].dispatchEvent(event);
}

總結:理解閉包的概念是重要的,但咱們不該當過多的使用閉包,它有優勢,也優缺點,是一把雙刃劍。使用閉包能夠建立一個封閉的環境,使得咱們能夠保存私有變量,避免全局做用域命名衝突,增強了封裝性;但它常駐內存的特性也對網頁的性能形成了比較大的影響,在引用計數的垃圾回收策略下更容易形成內存泄漏。

相關文章
相關標籤/搜索