JavaScript 閉包總結

什麼是閉包

簡單的說閉包就是函數裏面的函數,《JavaScript高級程序設計》裏是這樣定義的javascript

閉包是指有權訪問另外一個函數做用域中的變量的函數。html

先看一道面試時常常被考的題目

  • 代碼1:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>lzhTest</title>
</head>
<body>
<ul>
    <li>0</li>
    <li>1</li>
</ul>
<script>
    var lis = document.getElementsByTagName("li");
    for(var i = 0; i < lis.length; i++){
        lis[i].onclick = function(event){
            alert(i);
        }
    }
</script>
</body>
</html>

分別點擊 li,alert什麼?答案均是 2. 爲何呢?咱們接着往下看前端

做用域鏈和活動對象

函數被調用時會建立一個執行環境和做用域鏈 (scope chain),做用域鏈中每一個元素都指向一個活動對象或變量對象 (執行環境中定義的全部變量和函數都保存在這個對象中,包括 this、arguments),函數執行完畢,做用域鏈被銷燬,若是這時相應的變量對象沒有被引用,則變量對象佔用的空間會被釋放
好比上面題目中的做用域鏈是這樣的:
做用域鏈和活動對象java

匿名函數1匿名函數2是兩個事件處理函數,從圖中能夠看出,在做用域鏈的最前端(即下標爲0)對應的活動對象中,是不存在 i 的,i 在全局變量對象中,點擊的時候,須要往做用域的上層查找 i,因而就找到了全局變量對象中的 i,由於點擊的時候,i 早已增長成爲 2,因此 alert 的 i 均爲 2。jquery

怎樣作到每次 alert 的是下標呢?

直接看代碼:面試

  • 代碼2
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>lzhTest</title>
</head>
<body>
    <ul>
        <li>0</li>
        <li>1</li>
    </ul>
    <script>
        var lis = document.getElementsByTagName("li");
        var helper = function(i){
            return function(event) {
                alert(i);
            }
        }
        for(var i = 0; i < lis.length; i++){
            lis[i].onclick = helper(i);
        }
        // 參考自:《JavaScript語言精粹》
    </script>
</body>
</html>

對應的做用域鏈及活動對象:
理解了這裏,閉包應該掌握得差很少了瀏覽器

  • 代碼2中,全局變量對象中 i 的變化用 0->2 表示 (代碼1也如此)
  • 第一次調用 helper 時,全局變量對象中 i 是0,因此此時 helper(1) 的活動對象中 i 是0,由於是以形參的形式從全局變量對象中傳進來的。此後 helper(1) 中的 i 就不變了。
  • 接着 helper(1) 中返回一個匿名函數1,(根據《JavaScript高級程序設計》介紹:函數在調用時生成做用域鏈和活動對象,但閉包被返回時,就會生成做用域鏈和活動對象(中文第3版 P180 line7),個人理解並非這樣的),返回的 匿名函數1 引用着 helper1 中的i,helper(1) 執行完畢,以後 helper(1)的執行環境和做用域鏈銷燬,可是 helper(1) 的活動對象還在,由於 匿名函數1 的做用域鏈還在引用着它(按照個人理解應該是 匿名函數1 還引用着它)。(執行環境和做用域鏈銷燬這一過程在圖中沒有體現出來)。
  • 若是用戶 click lis[0],那麼就會調用 匿名函數1,(按照個人理解:此時匿名函數1的執行環境纔會被壓入環境棧中,同時生成 匿名函數1 的做用域鏈和活動對象),alert(i) 時,由於 匿名函數1 的活動對象中找不到 i 因此往做用域鏈的父級找,找到了 helper(1) 活動對象中的 i,因而 alert 了 0
  • 在第二次調用 helper 時,生成的做用域鏈和活動對象是新的了,與 helper(1) 中的不一樣,同理,當用戶 click lis[1] 時,alert 1,若是此時還不是很懂的話,能夠回頭再看看圖,或者從 代碼1 那裏重新理解。
  • 若是有認真看的話,請思考一下個人見解和《JavaScript高級程序設計》的見解,到底哪一個是正確的,我仍是堅持本身的見解,若是有錯的話,還請指出。閉包

    因爲閉包會攜帶包含它的函數的做用域,所以會比其餘函數佔用更多的內存。過分使用閉包可能會致使內存佔用過多。模塊化

  • 來看一個閉包內引用着活動對象中的變量時,活動對象不被釋放的例子,注意看圖片右側的 scope:
    活動對象不被釋放函數

從圖中能夠看出,param一、param3 所在的活動對象不在做用域鏈中,應該是被通知準備回收了或者已經回收了,而 param二、param4 所在的變量對象還在。

  • 另外,若是閉包中有 eval 的話,因爲不能判斷 eval 裏是否有引用父級做用域鏈活動對象中的變量,那麼該做用域鏈中的全部活動對象都會被保留,因此在閉包中儘可能不要使用 eval
    閉包中有eval

  • 但若是在裏面用的是 new Function("這裏引用閉包外的變量") 這種寫法,若是沒有其它引用,父級做用域鏈的活動對象是不會保留的,下面這種寫法最終會報錯 Uncaught ReferenceError: param1 is not defined
    閉包中有動態建立函數

內存泄露

若是閉包的做用域鏈中保存着一個HTML 元素,那麼就意味着該元素將沒法被銷燬,代碼以下,只要匿名函數存在,element 的引用數至少也是 1,所以它所佔用的內存就永遠不會被回收。(書中有討論到這是IE9之前的問題,我怎麼以爲這是個廣泛的問題呢,難道 Chrome 或其它瀏覽器中 element 的引用數至少還能是0嗎?求不吝賜教)。

function assignHandler(){
   var element = document.getElementById("someElement");
   element.onclick = function(){
       alert(element.id);
   };
}
  • 因此《JavaScript高級程序設計》建議這麼寫:
function assignHandler(){
   var element = document.getElementById("someElement");
   var id = element.id;

   element.onclick = function(){
       alert(id);
   };

   element = null;
}

閉包的做用

閉包無處不在,若是真的要概括幾個用處,以下:

模仿塊級做用域

咱們都知道,在 JavaScript 中,是不存在塊級做用域的,也就是在 {} 裏面聲明的變量,在 {} 外面依然能夠訪問。但若是利用函數一旦執行完,其中執行環境和做用域鏈均銷燬的特性,咱們能夠這麼作:

(function(){
    // 讓一個括號包着一個函數,至關於獲得這個函數的引用,
    // 而後再在後面加個括號,執行這個函數,稱這種函數爲 當即執行函數
    // 在這裏面聲明的變量,外部不可訪問,除非 return 一個閉包或變量
    // 若是此時外部是一個函數的話,那這個當即執行函數也是一個閉包
    // 只是這個閉包並無返回些什麼
})();

模塊化開發

有了上面提到的模仿塊級做用域,就能夠減小全局變量的使用,jQuery 就是這麼作的:

(function(window, undefined){
    // ...
    // 這裏實現 jQuery 的全部功能
    // 勢必會聲明不少變量,若是暴露在全局做用域中會形成命名衝突
    // 因此用一個當即執行函數包起來,可是爲何要傳入 window 呢
    // 傳入 window 是爲了讓 window 成爲當前做用域下的變量,這樣能夠減小訪問成本,
    // 另外便於壓縮,好比將 window 壓縮成 e,外部傳進來的依然是 window
    // 傳入 undefined 的緣由:
    // 在低版本的 IE 中,undefined 是可寫的,有可能 undefined 就不是 undefined 了
    window.jQuery = window.$ = jQuery;
    // 這裏的 jQuery 就是一個函數,平時咱們用的時候是 $(參數、參數),因此,他也是個閉包嘛
    // 上面是模塊中的一種,而旦還有更高級的,jQuery 兼容 AMD 規範
    if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
        define( "jquery", [], function () {
          // AMD 這裏不詳細介紹了,這裏就是 define 一個模塊
          // 這裏也是一個閉包
          return jQuery; // 這裏就是閉包中的閉包了
      } );
    }
}(window))
  • 閉包無處不在
相關文章
相關標籤/搜索