文章最新內容已遷移至公眾號 不知非攻,後續維護也在公眾號裡修正,歡迎關注。
初學JavaScript時,我在閉包上,走了很多彎路。而這次重新回過頭來對基礎知識進行梳理,要講清楚閉包,也是一個非常大的挑戰。
閉包有多重要?如果你是初入前端的朋友,我沒有辦法直觀的告訴你閉包在實際開發中的無處不在,但是我可以告訴你,前端面試,必問閉包。面試官們常常用對閉包的了解程度來判定面試者的基礎水平,保守估計,10個前端面試者,至少5個都死在閉包上。
可是為什麼,閉包如此重要,還是有那麼多人沒有搞清楚呢?是因為大家不願意學習嗎?還真不是,而是我們通過搜索找到的大部分講解閉包的中文文章,都沒有清晰明了的把閉包講清楚。要麼淺嘗輒止,要麼高深莫測,要麼乾脆就直接亂說一通。
因此本文的目的就在於,能夠清晰明了的把閉包說清楚,讓讀者朋友們看了之後,就把閉包給徹底學會了,而不是似懂非懂。
閉包對於有一點 JavaScript 使用經驗但從未真正理解閉包概念的人來說,理解閉包可以看作是某種意義上的重生,突破閉包的瓶頸可以使你功力大增。
閉包是一種特殊的對象。
它由兩部分組成。執行上下文(代號A),以及在該執行上下文中創建的函數(代號B)。
當B執行時,如果訪問了A中變量對象中的值,那麼閉包就會產生。
在大多數理解中,包括許多著名的書籍,文章裡都以函數B的名字代指這裡生成的閉包。而在chrome中,則以執行上下文A的函數名代指閉包。
因此我們只需要知道,一個閉包對象,由A、B共同組成,在以後的篇幅中,我將以chrome的標準來稱呼。
function foo() { var a = 20; var b = 30;
function bar() { return a + b; }
return bar;}
var bar = foo();bar();上面的例子,首先有執行上下文foo,在foo中定義了函數bar,而通過對外返回bar的方式讓bar得以執行。當bar執行時,訪問了foo內部的變量a,b。因此這個時候閉包產生。
在基礎進階(一)[1]中,我總結了JavaScript的垃圾回收機制。JavaScript擁有自動的垃圾回收機制,關於垃圾回收機制,有一個重要的行為,那就是,當一個值,在內存中失去引用時,垃圾回收機制會根據特殊的算法找到它,並將其回收,釋放內存。
而我們知道,函數的執行上下文,在執行完畢之後,生命周期結束,那麼該函數的執行上下文就會失去引用。其佔用的內存空間很快就會被垃圾回收器釋放。可是閉包的存在,會阻止這一過程。
先來一個簡單的例子。
var fn = null;function foo() { var a = 2; function innnerFoo() { console.log(a); } fn = innnerFoo; }
function bar() { fn(); }
foo();bar();在上面的例子中,foo()執行完畢之後,按照常理,其執行環境生命周期會結束,所佔內存被垃圾收集器釋放。但是通過fn = innerFoo,函數innerFoo的引用被保留了下來,複製給了全局變量fn。這個行為,導致了foo的變量對象,也被保留了下來。於是,函數fn在函數bar內部執行時,依然可以訪問這個被保留下來的變量對象。所以此刻仍然能夠訪問到變量a的值。
這樣,我們就可以稱foo為閉包。
下圖展示了閉包foo的作用域鏈。
我們可以在chrome瀏覽器的開發者工具中查看這段代碼運行時產生的函數調用棧與作用域鏈的生成情況。如下圖。
關於如何在chrome中觀察閉包,以及更多閉包的例子,請閱讀基礎系列(六)
在上面的圖中,紅色箭頭所指的正是閉包。其中Call Stack為當前的函數調用棧,Scope為當前正在被執行的函數的作用域鏈,Local為當前的局部變量。
所以,通過閉包,我們可以在其他的執行上下文中,訪問到函數的內部變量。比如在上面的例子中,我們在函數bar的執行環境中訪問到了函數foo的a變量。個人認為,從應用層面,這是閉包最重要的特性。利用這個特性,我們可以實現很多有意思的東西。
不過讀者朋友們需要注意的是,雖然例子中的閉包被保存在了全局變量中,但是閉包的作用域鏈並不會發生任何改變。在閉包中,能訪問到的變量,仍然是作用域鏈上能夠查詢到的變量。
對上面的例子稍作修改,如果我們在函數bar中聲明一個變量c,並在閉包fn中試圖訪問該變量,運行結果會拋出錯誤。
var fn = null;function foo() { var a = 2; function innnerFoo() { console.log(c); console.log(a); } fn = innnerFoo; }
function bar() { var c = 100; fn(); }
foo();bar();關於這一點,很多同學把函數調用棧與作用域鏈沒有分清楚,所以有的童鞋看了我關於介紹執行上下文的文章時就義正言辭的說我的例子有問題,而這些評論有很大的誤導作用,為了幫助大家自己擁有能夠辨別的能力,所以我寫了基礎(六),教大家如何在chrome中觀察閉包,作用域鏈,this等。當然我也不敢100%保證我文中的例子就一定正確,所以教大家如何去辨認我認為是一件最重要的事情。
閉包的應用場景當然,只有把閉包運用到實踐中,才能對閉包有更深刻的認識。
這裡我們大概了解一下閉包的兩個非常重要的應用場景,他們分別是模塊化與柯裡化。
柯裡化在函數式編程中,利用閉包能夠實現很多炫酷的功能,柯裡化便是其中很重要的一種。
具體的內容在後面的章節中詳細分析。
模塊化模塊化是閉包最強大的一個應用場景。如果你是初學者,對於模塊的了解可以暫時不用放在心上,因為理解模塊需要更多的基礎知識。但是如果你已經有了很多JavaScript的使用經驗,在徹底了解了閉包之後,不妨藉助本文介紹的作用域鏈與閉包的思路,重新理一理關於模塊的知識。這對於我們理解各種各樣的設計模式具有莫大的幫助。
(function () { var a = 10; var b = 20;
function add(num1, num2) { var num1 = !!num1 ? num1 : a; var num2 = !!num2 ? num2 : b;
return num1 + num2; }
window.add = add;})();
add(10, 20);在上面的例子中,我使用函數自執行的方式,創建了一個模塊。add是模塊對外暴露的一個公共方法。而變量a,b被作為私有變量。在面向對象的開發中,我們常常需要考慮是將變量作為私有變量,還是放在構造函數中的this中,因此理解閉包,以及原型鏈是一個非常重要的事情。模塊十分重要,因此我會在以後的文章專門介紹,這裡就暫時不多說啦。
為了驗證自己有沒有搞懂作用域鏈與閉包,這裡留下一個經典的思考題,常常也會在面試中被問到。
利用閉包,修改下面的代碼,讓循環輸出的結果依次為1, 2, 3, 4, 5
for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000);}點此查看關於此題的詳細解讀[2]
理解閉包並不是一件簡單的事,如果感覺有困難,建議反覆閱讀。
References[1] 基礎進階(一): http://www.jianshu.com/p/996671d4dcc4
[2] 點此查看關於此題的詳細解讀: http://www.jianshu.com/p/9b4a54a98660