在js中,閉包是一個很重要又至關不容易徹底理解的要點,網上關於講解閉包的文章很是多,可是並非很是容易讀懂,在這裏以《javascript高級程序設計》裏面的理論爲基礎。用拆分的方式,深刻講解一下對於閉包的理解,若是有不對請指正。javascript
閉包的內部細節,依賴於函數被調用過程所發生的一系列事件爲基礎,因此有必要先弄清楚如下幾個概念:前端
- 執行環境(execution context)定義了變量或者函數有權訪問的其餘數據,每一個執行環境都有一個與之關聯的變量對象(variable object),執行環境中定義的變量和函數就保存在這個變量對象中;
全局執行環境是最外圍的一個執行環境,一般被認爲是window對象
執行環境和變量對象在運行函數時生成
執行環境中的全部代碼執行完之後,執行環境被銷燬,保存在其中的變量和函數也隨之銷燬;(全局執行環境到應用退出時銷燬)java
當代碼在一個執行環境中執行時,會建立變量對象的一個做用域鏈(scope chain),做用域鏈用來指定執行環境有權訪問的全部變量和函數的訪問順序;
做用域鏈的最前端,始終是當前代碼執行環境的變量對象,若是這個環境是函數,則其活動對象就是變量對象
做用域鏈的下一個變量對象,來自外部包含環境,再下一個變量對象,來自下一個外部包含環境,以此類推直到全局執行環境
在函數執行過程,根據當前執行環境的做用域鏈來逐層向外查找變量,而且進行標識符解析數組
是否是以爲以上的理論很枯燥並且艱澀?由於基本上是從書上引用來的,不着急着理解,先擺在上面,等會結合案例回頭再來看!接下來請看樣例:閉包
樣例1 <script> var a = 2; function A(){ var a = 1; return a ; } console.log(A());//1 </script>
以這段簡單的代碼爲例,根據上面的理論畫一下關係圖(直接用ps畫的,原諒我拙劣的筆跡):
如圖所示,在執行函數A的時候,建立了A的執行環境和變量對象,其中A的變量對象和全局變量對象中都含有a變量,根據做用域鏈從前向後查找,在A的變量對象中找到,因此輸出1,執行完畢之後 ,A的執行環境銷燬,A的變量對象因爲沒有被引用,因此也銷燬;函數
樣例2 <script> function A(){ var a = 1; return a ; } console.log(a);// 報錯 a is not defined </script>
這個例子比較簡單,要畫圖的話只須要畫一個全局變量對便可,由於在js中,外圍環境沒法訪問內圍局部變量(其實本質就是做用域鏈上找不到相應的值),因此這裏會報變量未定義的錯誤。spa
樣例3 <script> function A(){ var a = 1; function B(){ if(a==1){ console.log(1) } else { console.log(0); } } B(); } A();//1 </script>
上面這個例子,在函數A中定義了函數B,關係圖以下:
從圖上能夠很清楚的看出,在每一個執行環境中能夠訪問到的變量對象,因此B能夠訪問A的變量對象和全局變量對象中的變量以及自身變量對象,A能夠訪問自身變量對象和全局變量對象設計
關於執行環境和做用域鏈暫時說到這裏,下面進入正題,講閉包;3d
閉包是指有權訪問另外一個函數做用域變量的函數,建立閉包的一般方式,是在一個函數內部建立另外一個函數code
上文咱們提到了,因爲做用域鏈的結構,外圍函數是沒法訪問內部變量的,爲了可以訪問內部變量,咱們就可使用閉包,閉包的本質仍是函數,閉包的本質仍是函數閉包的本質仍是函數。
樣例4 <script> function A(){ var x = 1; return function(){ console.log(x); } } var m = A(); m();//1 </script>
上面就是一個很簡單的閉包例子,經過m函數,咱們能夠得到A函數內部變量的值,這個樣例比較簡單,看不出什麼問題,接下來咱們來深刻了解一下。
-------------------------------從簡單到複雜的分割線,請作好準備----------------------------------------------------
樣例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 <script> function A(){ var x = 1; var m=[]; m[0] = function(){ x++; console.log(x); }; m[1] = function(){ x++; console.log(x); } return m; } var m = A();//第一次運行A,並且只運行這一次 m[0]();//2 m[1]();//3 m[0]();//4 m[1]();//5 </script>
這個例子和剛剛十分相似,不一樣的是,在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 <script> function A(){ var funs=[]; for(var i=0;i<10;i++){ funs[i] = function anonymous1(num){ return function anonymous2(){ return num; } }(i); } return funs; } var funs = A();//定義funs[0]-funs[9],10個函數 console.log(funs[0]());//0 console.log(funs[1]());//1 console.log(funs[6]());//6 </script>
是否是一看頭就大了?不要緊,接下來咱們慢慢分析,固然,上述代碼中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函數使用
寫到這裏,關於閉包的主要特徵和辨別方式已經基本講到了,我的感受由於這個問題比較抽象,仍是多看看文中以及網上的一些例子,加深理解。以上內容屬於我的看法,若是有不一樣意見,歡迎指出和探討。但願能對看到的人有所幫助,同時,碼字不易(尤爲是還要配上靈魂畫師級別的配圖~),請尊重做者的版權,轉載請註明出處,如做商用,請與做者聯繫,感謝!