詳解js中的閉包
前言
在js中,閉包是一個很重要又相當不容易完全理解的要點,網上關於講解閉包的文章非常多,但是並不是非常容易讀懂,在這裡以《javascript高階程式設計》裡面的理論為基礎。用拆分的方式,深入講解一下對於閉包的理解,如果有不對請指正。
寫在閉包之前
閉包的內部細節,依賴於函式被呼叫過程所發生的一系列事件為基礎,所以有必要先弄清楚以下幾個概念:
1. 執行環境和活動物件
** - 執行環境(execution context)定義了變數或者函式有權訪問的其他資料,每個執行環境都有一個與之關聯的變數物件(variable object),執行環境中定義的變數和函式就儲存在這個變數物件中; 全域性執行環境是最外圍的一個執行環境,通常被認為是window物件 執行環境和變數物件在
執行函式**時生成 執行環境中的所有程式碼執行完以後,執行環境被銷燬,儲存在其中的變數和函式也隨之銷燬;(全域性執行環境到應用退出時銷燬)
2. 作用域鏈
當代碼在一個執行環境中執行時,會建立變數物件的一個作用域鏈(scope chain),作用域鏈用來指定執行環境有權訪問的所有變數和函式的訪問順序; 作用域鏈的最前端,始終是當前程式碼執行環境的變數物件,如果這個環境是函式,則其活動物件就是變數物件 作用域鏈的下一個變數物件,來自外部包含環境,再下一個變數物件,來自下一個外部包含環境,以此類推直到全域性執行環境 在函式執行過程,根據當前執行環境的作用域鏈來逐層向外查詢變數,並且進行識別符號解析
是不是覺得以上的理論很枯燥而且艱澀?因為基本上是從書上引用來的,不著急著理解,先擺在上面,等會結合案例回頭再來看!接下來請看樣例: 樣例1 以這段簡單的程式碼為例,根據上面的理論畫一下關係圖(直接用ps畫的,原諒我拙劣的筆跡):
如圖所示,在執行函式A的時候,建立了A的執行環境和變數物件,其中A的變數物件和全域性變數物件中都含有a變數,根據作用域鏈從前向後查詢,在A的變數物件中找到,所以輸出1,執行完畢以後 ,A的指向環境銷燬,A的變數物件由於沒有被引用,所以也銷燬; 樣例2 這個例子比較簡單,要畫圖的話只需要畫一個全域性變數對即可,因為在js中,外圍環境無法訪問內圍區域性變數(其實本質就是作用域鏈上找不到相應的值),所以這裡會報變數未定義的錯誤。 樣例3 上面這個例子,在函式A中定義了函式B,關係圖如下:
從圖上可以很清楚的看出,在每個執行環境中可以訪問到的變數物件,所以B可以訪問A的變數物件和全域性變數物件中的變數以及自身變數物件,A可以訪問自身變數物件和全域性變數物件
關於執行環境和作用域鏈暫時說到這裡,下面進入正題,講閉包;
3. 初涉閉包
閉包是指有權訪問另一個函式作用域變數的函式,建立閉包的通常方式,是在一個函式內部建立另一個函式
上文我們提到了,由於作用域鏈的結構,外圍函式是無法訪問內部變數的,為了能夠訪問內部變數,我們就可以使用閉包,閉包的本質還是函式,閉包的本質還是函式閉包的本質還是函式。 樣例4 上面就是一個很簡單的閉包例子,通過m函式,我們可以獲得A函式內部變數的值,這個樣例比較簡單,看不出什麼問題,接下來我們來深入瞭解一下。 -------------------------------從簡單到複雜的分割線,請做好準備----------------------------------------------------
4. 閉包詳解
難點一:判斷作用域指向的變數物件是否相同
樣例5
<script>
function A(){
var x = 1;
return function(){
x++;
console.log(x);
}
}
var m1 = A();//第一次執行A函式
m1();//2
m1();//3
var m2 = A();//第二次執行A函式
m2();//2
m1();//4
</script>
上面這個例子其實可以引出幾個問題: 1.為什麼連續執行m1的時候,x的值在遞增? 2.定義函式m2的時候,為什麼x的值重新從1開始了? 3.執行m2以後,為什麼再執行m1,x還是按照之前m1的執行結果繼續增長?(其實就是m1和m2裡面的x為什麼是相互獨立,各自維持的?)
其實要解決上面的問題,我們就要用到前面鋪墊的知識點了: 首先,先畫一下結構圖, (額,這圖畫的可能真的有點醜),不要慌,圖上雖然畫的有點亂,但是其實很簡單:左半部分和上面簡單閉包的例子,其實是完全一樣的,而右邊半部分,與左邊其實是完全對稱的;注意看圖上的重點:每次執行A函式時,都會生成一個A的活動變數和執行環境,執行完畢以後,A的執行環境銷燬,但是活動物件由於被閉包函式引用,所以仍然保留,所以,最終剩下兩個A的變數物件,因此m1和m2在操作x時,指向的是不同的資料,
現在來回答上面的三個問題: 1.(為什麼連續執行m1的時候,x的值在遞增?)answer:因為m1在引用的活動物件A一直沒有釋放(想釋放的話可以讓m1=null),所以x的值一直遞增。 2.定義函式m2的時候,為什麼x的值重新從1開始了?answer:因為又一次運行了A函式,生成一個新的A的活動物件,所以m2的作用域鏈引用的是一個新的x值。 3.m1和m2裡面的x為什麼是相互獨立,各自維持的?answer:因為在定義m1和m2的時候,分別運行了A函式,生成了兩個活動物件,所以,m1和m2的作用域鏈是指向不同的A的活動物件的。
好的,到這裡先回顧一下前面說到的知識點:
執行環境和變數物件在執行函式時生成 執行環境中的所有程式碼執行完以後,執行環境被銷燬,儲存在其中的變數和函式也隨之銷燬;(全域性執行環境到應用退出時銷燬)
感覺理解了嗎?接下來,再看看另一個很類似的例子: 樣例6
這個例子和剛剛十分類似,不同的是,在A內部就先定義了兩個函式,可以看出 ,最後的結果與上面的例子有些不同:變數x仍然能保持遞增,但是m[0]和m[1]定義的函式,對於x的改變不再是相互獨立的,其實大家估計猜到了,這裡的m[0]和m[1]的作用域指向的A的變數物件,其實是同一個,為什麼呢?很簡單,看看剛剛這段程式碼,其實是隻呼叫了一次A函式,再看上文那句話:
執行環境和變數物件在執行函式時生成
既然A只執行一次,那麼A的活動變數當然也就生成了一個,所以這裡m[0]和m[1]的作用域指向同一個A的變數物件
難點二:判斷變數物件中變數的值
樣例7
<script>
function A(){
var funs=[];
for(var i=0;i<10;i++){
funs[i]=function(){
return i;
}
}
return funs;
}
var funs = A();//定義funs[0]-funs[9],10個函式
console.log(funs[0]());//10
console.log(funs[1]());//10
console.log(funs[6]());//10
</script>
這個例子其實算是一個經典案例,在很多地方都有提到,執行完畢後 funs陣列中,funs[0]-funs[9]存的其實都是一樣的,都是一個返回i值的函式,這個例子容易錯誤的地方其實在於,弄錯了產生執行環境的時機,還是看這句話:
執行環境和變數物件在執行函式時生成
所以,當執行var funs = A();
時,只是定義函式,而沒有執行,真正產生環境變數的時間是在console.log(funs[0]());
這三句的時候,此時A的變數物件中i值是什麼呢?很簡單,看它return的時候,i的值,顯然,i的值是10,所以,最後三句輸出的都是10
好的,針對以上的案例,如果我就是想讓fun[i]能夠返回i,那應該怎麼寫呢?在《javascript高階程式設計》中,提供了一種參考的寫法: 樣例8 是不是一看頭就大了?沒關係,接下來我們慢慢分析,當然,上述程式碼中anonymous1和anonymous2兩個名字是我自己新增上的,為了後面能夠更好的說明。 首先,先來看看function anonymous1(num){}(i),這是一個立即執行函式,效果和名字一樣,定義完之後馬上執行結果,那這裡執行的結果是什麼呢?就是把i的值立即傳遞給num這個區域性變數,然後再返回anonymous2,請注意這個立即執行函式被執行的次數,10次,再來看看這句話
執行環境和變數物件在執行函式時生成
好的,那現在請回答我:這裡面生成了幾個anonymous1的活動變數? answer:當然也是10個,那每個anonymous1活動變數中存貯的num值是多少? answer:看anonymous函式return的時候可以知道,存貯的num值就是每次傳入的i值,也就是0-9
好了,那現在很明瞭了,這樣的寫法其實相當於,把每次的i值都儲存在一個anonymous1活動變數鍾,給最內層的anonymous2函式使用
小結
寫到這裡,關於閉包的主要特徵和辨別方式已經基本講到了,個人感覺因為這個問題比較抽象,還是多看看文中以及網上的一些例子,加深理解。以上內容屬於個人見解,如果有不同意見,歡迎指出和探討。希望能對看到的人有所幫助,同時,碼字不易(尤其是還要配上靈魂畫師級別的配圖~),請尊重作者的版權,轉載請註明出處,如作商用,請與作者聯絡,感謝!