相信學(xué)習(xí) JavaScript 的同學(xué)都知道「閉包(Closure)」,這個(gè)概念在 JavaScript 中是非常重要的浩淘,并且在大多數(shù)人看來閉包是非常難以理解的概念。既然這樣尤揣,那今天就帶大家一起來看看這到底是何方神圣宙橱。
維基百科是這樣解釋的:
閉包姨俩,又稱詞法閉包或函數(shù)閉包,是引用了自由變量的函數(shù)师郑,這個(gè)被引用的自由變量將和這個(gè)函數(shù)一同存在,即使離開了創(chuàng)造它的環(huán)境也不例外调窍,所以宝冕,閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實(shí)體。
說了半天一句也沒看懂邓萨,那我們來看看 JS 官方是怎么解釋的:
閉包是指多個(gè)變量和綁定了這些變量的環(huán)境的表達(dá)式( 通常是一個(gè)函數(shù) )地梨,因而這些變量也是該表達(dá)式的一部分。
我去缔恳,這又是什么東西宝剖?只看出閉包是個(gè)函數(shù),其他的還是一概不知歉甚。
既然這樣不知道它們?cè)谡f些什么万细,不如跟著我的思路來看一看到底是個(gè)什么。
要理解閉包纸泄,首先我們要弄明白什么是詞法作用域和作用域鏈赖钞。
作用域一般有兩種常見的模型,一種叫做詞法作用域聘裁,另一種叫做動(dòng)態(tài)作用域雪营。我們的 JavaScript 就是基于詞法作用域的語言。
簡(jiǎn)單來講衡便,詞法作用域就是一個(gè)變量的作用在出生(定義)時(shí)就已經(jīng)被設(shè)定好了献起,當(dāng)在本作用域中找不到變量時(shí),就會(huì)一直向父作用域中去查找镣陕,直到直到為止谴餐。如果不明白的話,看下面的代碼大概就能理解了茁彭。
代碼中 fun1 在其內(nèi)部已經(jīng)定義了變量 y总寒,所以在查找 y 時(shí)在該作用域(內(nèi)部函數(shù) fun1 中)內(nèi)可以找到,則無需再往父作用域中去查找理肺;如果在其作用域內(nèi)沒有查找到摄闸,則會(huì)在父作用域內(nèi)查找善镰,也就是使用 fun 函數(shù)中的變量 y。
既然 JavaScript 中的函數(shù)和變量都有其作用域年枕,那么作用域之間就會(huì)產(chǎn)生一條鏈炫欺,我們稱之為作用域鏈。假設(shè)我們編寫了一段 JS 代碼熏兄,那這段代碼就會(huì)有一個(gè)與之關(guān)聯(lián)的作用域鏈品洛。這個(gè)作用域鏈就是由全局對(duì)象(如:window)、我們自定義的對(duì)象(函數(shù)摩桶,局部變量)組成桥状。比如上面的代碼,其作用域鏈上是這樣的:函數(shù) fun1硝清、變量 y ==> 函數(shù) fun辅斟、變量 x、y ==> 全局對(duì)象芦拿。這就是所謂的作用域鏈士飒。
理解了上面的內(nèi)容,就可以來看看咱們今天的主人公「閉包」了蔗崎。
函數(shù)對(duì)象可以通過作用域鏈相互關(guān)聯(lián)起來酵幕,函數(shù)體內(nèi)部的變量都可以保存在函數(shù)作用域內(nèi),也就是函數(shù)變量可以被藏在作用域鏈之內(nèi)缓苛,這種特性在計(jì)算機(jī)科學(xué)文獻(xiàn)中稱為閉包芳撒。看上去變量被“封閉包裹”了起來他嫡。由此可見番官,從理論上講,所有的 JavaScript 函數(shù)全都是閉包的钢属,因?yàn)樗鼈兌际菍?duì)象徘熔,它們都關(guān)聯(lián)在作用域鏈上。
那么怎么才能顯式的形成閉包呢淆党?先來看下面的例子酷师。
注意這段代碼中標(biāo)記的地方:內(nèi)部函數(shù) fun1 在執(zhí)行前通過外部函數(shù)被返回了,外部函數(shù)被賦值給了變量 result染乌。這時(shí)山孔,變量 result 的值就變成了函數(shù) fun1,也就是說內(nèi)部變量 name 在所屬函數(shù)外部被調(diào)用了荷憋。我們來證實(shí)一下:
可以看到 result 的值就是函數(shù) fun1台颠,那為什么還可以讀取變量 name 呢?答案就是 result 變成閉包了。
result 由兩部分組成:函數(shù)以及創(chuàng)建該函數(shù)的環(huán)境串前。函數(shù)就是被外部函數(shù)返回的內(nèi)部函數(shù)瘫里,而環(huán)境就是由閉包創(chuàng)建時(shí)在作用域中的任何局部變量組成的。在我們的例子中荡碾,result 是一個(gè)閉包谨读,由函數(shù) fun1 和閉包創(chuàng)建時(shí)存在的「"Google"」字符串形成。
現(xiàn)在想想坛吁,維基百科說的好像就是這么回事:閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實(shí)體劳殖。這就解釋了為什么可以讀取變量 name 了,因?yàn)?result 引用的環(huán)境是 fun1 函數(shù)相關(guān)的引用環(huán)境拨脉,可以理解為: result 處在 fun1 所處的作用域鏈的位置哆姻,既然這樣,那自然可以讀取變量 name 了女坑。
這就是閉包填具,現(xiàn)在看來也就是這么回事么,沒什么難理解的匆骗。
既然已經(jīng)理解了,那我們?cè)賮砜匆粋€(gè)例子(引用自廖雪峰老師的 JS 教程):
這個(gè)例子中誉简,每次循環(huán)碉就,都創(chuàng)建了一個(gè)新的函數(shù),然后闷串,把創(chuàng)建的 3 個(gè)函數(shù)都添加到數(shù)組 arr 中返回了瓮钥。
那么調(diào)用 f1() 、f2()烹吵、f3() 的結(jié)果是什么呢碉熄?不就是 1,4肋拔,9 嗎? 不是锈津。
你沒看錯(cuò),答案就是 16凉蜂,全部都是琼梆!原因在于閉包 results 返回的數(shù)組中的函數(shù)引用了變量 i,但這個(gè)返回的數(shù)組中的函數(shù)并不是立刻執(zhí)行的窿吩,等到執(zhí)行時(shí)茎杂,它們所引用的變量 i 已經(jīng)變成 4 了,所以結(jié)果為 16纫雁。還是沒明白煌往?上面我們說了,閉包是由函數(shù)和其相關(guān)的引用環(huán)境組合而成的轧邪,既然所處的環(huán)境還是在作用域鏈原來的位置刽脖,那么變量 i 就會(huì)在 for 循環(huán)的作用下變成 4羞海,而到了你去調(diào)用閉包的時(shí)候,閉包引用的變量 i 的值自然為 4 了曾棕,所以結(jié)果自然就是 16 了扣猫。
這個(gè)例子要提醒大家的是:返回的函數(shù),不要引用任何循環(huán)變量和變量值后續(xù)會(huì)發(fā)生變化的變量翘地。這一點(diǎn)在使用閉包時(shí)一定要牢記申尤。
還要說明的一點(diǎn)就是,避免濫用閉包衙耕。原因:使用閉包之后昧穿,閉包中函數(shù)所處的環(huán)境會(huì)一直存在,所以閉包會(huì)使得函數(shù)中的變量都被保存在內(nèi)存中橙喘,不會(huì)被“垃圾回收機(jī)制”回收时鸵,進(jìn)而內(nèi)存消耗過大,造成網(wǎng)頁性能下降厅瞎。
最后饰潜,理解清楚作用域鏈的概念不但對(duì)掌握閉包非常重要,并且對(duì)其他知識(shí)點(diǎn)(比如 with 語句)同樣很重要和簸。