閉包原理以及使用場景
前端面試,必問閉包
閉包有多重要?如果你是初入前端的朋友,我沒有辦法直觀的告訴你閉包在實際開發(fā)中的無處不在,但是我可以告訴你,前端面試,必問閉包。面試官們常常用對閉包的了解程度來判定面試者的基礎(chǔ)水平,保守估計,10個前端面試者,至少5個都死在閉包上。
講解閉包時,我們先來看個例子

上面代碼中,函數(shù)f1可以讀取全局變量n。但是下面例子,函數(shù)外部無法讀取函數(shù)內(nèi)部聲明的變量。

上面代碼中,函數(shù)f1內(nèi)部聲明的變量n,函數(shù)外是無法讀取的.
如果有時需要得到函數(shù)內(nèi)的局部變量。正常情況下,這是辦不到的,只有通過變通方法才能實現(xiàn)。那就是在函數(shù)的內(nèi)部,再定義一個函數(shù)。

上面代碼中,函數(shù)f2就在函數(shù)f1內(nèi)部,這時f1內(nèi)部的所有局部變量,對f2都是可見的。既然f2可以讀取f1的局部變量,那么只要把f2作為返回值,我們不就可以在f1外部讀取它的內(nèi)部變量了嗎!
閉包是什么
閉包是一種特殊的對象。
它由兩部分組成。執(zhí)行上下文(代號A),以及在該執(zhí)行上下文中創(chuàng)建的函數(shù)(代號B)。
當(dāng)B執(zhí)行時,如果訪問了A中變量對象中的值,那么閉包就會產(chǎn)生。
在大多數(shù)理解中,包括許多著名的書籍,文章里都以函數(shù)B的名字代指這里生成的閉包。而在chrome中,則以執(zhí)行上下文A的函數(shù)名代指閉包。
我們可以對上面代碼進行如下修改:

上面的例子,首先有執(zhí)行上下文f1,在f1中定義了函數(shù)f2,而通過對外返回f2的方式讓f2得以執(zhí)行。當(dāng)f2執(zhí)行時,訪問了f1內(nèi)部的變量a。因此這個時候閉包產(chǎn)生
閉包就是函數(shù)f1,

在上面的圖中,紅色箭頭所指的正是閉包。其中Call Stack為當(dāng)前的函數(shù)調(diào)用棧,Scope為當(dāng)前正在被執(zhí)行的函數(shù)的作用域鏈,Local為當(dāng)前的局部變量。
JavaScript擁有自動的垃圾回收機制,關(guān)于垃圾回收機制,有一個重要的行為,那就是,當(dāng)一個值,在內(nèi)存中失去引用時,垃圾回收機制會根據(jù)特殊的算法找到它,并將其回收,釋放內(nèi)存。
而我們知道,函數(shù)的執(zhí)行上下文,在執(zhí)行完畢之后,生命周期結(jié)束,那么該函數(shù)的執(zhí)行上下文就會失去引用。其占用的內(nèi)存空間很快就會被垃圾回收器釋放??墒情]包的存在,會阻止這一過程。
我們再來看個例子:

在上面的例子中,foo()
執(zhí)行完畢之后,按照常理,其執(zhí)行環(huán)境生命周期會結(jié)束,所占內(nèi)存被垃圾收集器釋放。但是通過fn = innerFoo
,函數(shù)innerFoo
的引用被保留了下來,復(fù)制給了全局變量fn
。這個行為,導(dǎo)致了foo的變量對象,也被保留了下來。于是,函數(shù)fn在函數(shù)bar內(nèi)部執(zhí)行時,依然可以訪問這個被保留下來的變量對象。所以此刻仍然能夠訪問到變量a的值。
這樣,我們就可以稱foo為閉包。
所以,通過閉包,我們可以在其他的執(zhí)行上下文中,訪問到函數(shù)的內(nèi)部變量。比如在上面的例子中,我們在函數(shù)bar的執(zhí)行環(huán)境中訪問到了函數(shù)foo的a變量。個人認(rèn)為,從應(yīng)用層面,這是閉包最重要的特性。利用這個特性,我們可以實現(xiàn)很多有意思的東西。
不過讀者朋友們需要注意的是,雖然例子中的閉包被保存在了全局變量中,但是閉包的作用域鏈并不會發(fā)生任何改變。在閉包中,能訪問到的變量,仍然是作用域鏈上能夠查詢到的變量。
對上面的例子稍作修改,如果我們在函數(shù)bar中聲明一個變量c,并在閉包fn中試圖訪問該變量,運行結(jié)果會拋出錯誤。

閉包形成的條件
函數(shù)嵌套
內(nèi)部函數(shù)引用外部函數(shù)的局部變量
閉包的應(yīng)用場景
初級前端了解以上閉包面試即可,以下內(nèi)容屬于中高級前端理解范圍。大家可以暫時跳過,等學(xué)有一定基礎(chǔ)積累,再回頭看看。
除了面試,在實踐中,閉包有兩個非常重要的應(yīng)用場景。分別是模塊化與柯里化。
柯里化
模塊化
在我看來,模塊是閉包最強大的一個應(yīng)用場景。如果你是初學(xué)者,對于模塊的了解可以暫時不用放在心上,因為理解模塊需要更多的基礎(chǔ)知識。但是如果你已經(jīng)有了很多JavaScript的使用經(jīng)驗,在徹底了解了閉包之后,不妨借助本文介紹的作用域鏈與閉包的思路,重新理一理關(guān)于模塊的知識。這對于我們理解各種各樣的設(shè)計模式具有莫大的幫助。

在上面的例子中,我使用函數(shù)自執(zhí)行的方式,創(chuàng)建了一個模塊。add是模塊對外暴露的一個公共方法。而變量a,b被作為私有變量。

為了驗證自己有沒有搞懂作用域鏈與閉包,這里留下一個經(jīng)典的思考題,常常也會在面試中被問到。
利用閉包,修改下面的代碼,讓循環(huán)輸出的結(jié)果依次為1, 2, 3, 4, 5
