JavaScript 閉包總結


什么是閉包

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

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

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

  • 代碼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),函數執行完畢,作用域鏈被銷毀,如果這時相應的變量對象沒有被引用,則變量對象占用的空間會被釋放
比如上面題目中的作用域鏈是這樣的:
作用域鏈和活動對象

匿名函數1匿名函數2 是兩個事件處理函數,從圖中可以看出,在作用域鏈的最前端(即下標為0)對應的活動對象中,是不存在 i 的,i 在全局變量對象中,點擊的時候,需要往作用域的上層查找 i,於是就找到了全局變量對象中的 i,因為點擊的時候,i 早已增加成為 2,所以 alert 的 i 均為 2。

怎樣做到每次 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:
    活動對象不被釋放

從圖中可以看出,param1、param3 所在的活動對象不在作用域鏈中,應該是被通知准備回收了或者已經回收了,而 param2、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))
  • 閉包無處不在


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM