閉包是js中一個(gè)晦澀難懂的一個(gè)概念,網(wǎng)上關(guān)于閉包的文章也是抓一大把,每個(gè)人的文章卻又不盡相同,或者說(shuō)凹炸,每個(gè)人的理解都不一樣。
什么是閉包
阮一峰老師的一篇文章中說(shuō):閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)昼弟∑∷可以把閉包簡(jiǎn)單理解成"定義在一個(gè)函數(shù)內(nèi)部的函數(shù)"。
在本質(zhì)上舱痘,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來(lái)的一座橋梁变骡。
這個(gè)解釋不能說(shuō)錯(cuò),但覺(jué)得有點(diǎn)片面芭逝。阮老師是從函數(shù)內(nèi)部變量的角度去看閉包塌碌。
也有一個(gè)國(guó)外的哥們的一篇文章,是這么說(shuō)的: 閉包是由函數(shù)引用其周邊狀態(tài)(詞法環(huán)境)綁在一起形成的(封裝)組合結(jié)構(gòu)旬盯。
我們來(lái)看一下MDN上對(duì)于閉包的解釋:閉包是指那些能夠訪問(wèn)獨(dú)立(自由)變量的函數(shù) (變量在本地使用台妆,但定義在一個(gè)封閉的作用域中)。換句話說(shuō)胖翰,這些函數(shù)可以“記憶”它被創(chuàng)建時(shí)候的環(huán)境接剩。
好吧,這個(gè)解釋更難懂萨咳。懊缺。。培他。
在阮老師的解讀中鹃两,一個(gè)點(diǎn)就是,在函數(shù)外部讀取函數(shù)內(nèi)部的變量靶壮,阮老師所說(shuō)的閉包就是將函數(shù)內(nèi)部和外部鏈接起來(lái)的一座橋梁也有失偏頗怔毛。
在《你不知道的js》這本書(shū)的《閉包作用域》這一章節(jié)中,有這樣的描述:
“當(dāng)函數(shù)可以記住并訪問(wèn)所在的詞法作用域時(shí)腾降,就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行碎绎◇θ溃”
摘錄來(lái)自: Kyle Simpson抗果、趙望野、梁杰. “你不知道的JavaScript(上卷)”奸晴。 iBooks.
這個(gè)描述中冤馏,有兩個(gè)關(guān)鍵的點(diǎn):一個(gè)是,函數(shù)寄啼,一個(gè)是記住并訪問(wèn)所在的詞法作用域逮光。這也和MDN上對(duì)于閉包的解釋相吻合。我們來(lái)看一下MDN上的關(guān)于閉包的其他描述:
閉包是一種特殊的對(duì)象墩划。它由兩部分構(gòu)成:函數(shù)涕刚,以及創(chuàng)建該函數(shù)的環(huán)境。環(huán)境由閉包創(chuàng)建時(shí)在作用域中的任何局部變量組成乙帮。
所以杜漠,到這里,我們可以看出察净,要理解閉包驾茴,我們要抓住兩個(gè)點(diǎn):一個(gè)是函數(shù),另一個(gè)就是創(chuàng)建該函數(shù)的環(huán)境氢卡,所在的詞法作用域锈至。
閉包的案例說(shuō)明
關(guān)于閉包的概念上的描述,就差不多如上面译秦,可能光看上面的描述還是不能理解什么是閉包峡捡。
那就找兩個(gè)例子來(lái)說(shuō)明一下。
來(lái)一個(gè)最簡(jiǎn)單的例子诀浪,也是大家舉的最多的例子:
var fn = function () {
var a = 'a in fn';
var b = function () {
return a;
}
return b;
}
var f = fn();
console.log(f());
這個(gè)例子估計(jì)是大家用來(lái)解釋閉包用到的最多的例子棋返。在函數(shù)fn()內(nèi)部,定義了另外一個(gè)函數(shù)b()雷猪,函數(shù)b()能夠訪問(wèn)fn()內(nèi)部的變量a睛竣,是因?yàn)閎在fn()的詞法作用域內(nèi)。然后我們將b()作為返回值返回求摇,并賦值給變量f,實(shí)質(zhì)上f和b兩個(gè)都是指向了同一個(gè)函數(shù)射沟,只是標(biāo)識(shí)符不同而已。因?yàn)檫@個(gè)函數(shù)能夠訪問(wèn)fn()內(nèi)部的詞法作用域与境,能訪問(wèn)fn()內(nèi)部的變量a验夯,因此,f()執(zhí)行的時(shí)候就能訪問(wèn)fn()內(nèi)部的變量a摔刁,這就是閉包挥转。
好了,我們用上面說(shuō)的兩個(gè)關(guān)鍵點(diǎn)來(lái)慢慢分析閉包:
首先,一個(gè)關(guān)鍵點(diǎn)函數(shù)绑谣,函數(shù)是哪個(gè)党窜?b還是f?都是,因?yàn)檫@兩個(gè)實(shí)質(zhì)是兩個(gè)標(biāo)識(shí)符指向了同一個(gè)函數(shù)借宵。第二個(gè)關(guān)鍵點(diǎn)幌衣,記住函數(shù)所在的詞法作用域。作用域是哪個(gè)壤玫,就是函數(shù)fn()的詞法作用域豁护。當(dāng)函數(shù)fn()執(zhí)行完畢后,f()還能繼續(xù)訪問(wèn)fn()內(nèi)的變量a欲间,這里b()或者f()就是閉包楚里。
到這里,可能有人就開(kāi)始噴了括改,你妹的腻豌,這不就是阮老師說(shuō)的閉包是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)嘛!V瞿堋吝梅!是的,阮老師的說(shuō)法并沒(méi)有錯(cuò)惹骂,只是有點(diǎn)片面苏携,為何?
我們來(lái)看下個(gè)例子:
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
在函數(shù)wait()內(nèi)部对粪,將一個(gè)函數(shù)timer()傳遞給定時(shí)器右冻。這里timer具有涵蓋wait(..)作用域的閉包,因此還保有對(duì)變量message的引用著拭。
當(dāng)wait()執(zhí)行1000毫秒后纱扭,它的內(nèi)部作用域并沒(méi)消失,timer()函數(shù)依然保持有對(duì)wait()函數(shù)作用域的閉包儡遮。
比如乳蛾,再來(lái)一個(gè)例子,就是在for循環(huán)中的閉包鄙币。這個(gè)例子被經(jīng)常用作考察es6的let和var的區(qū)別肃叶,說(shuō)實(shí)話已經(jīng)用爛了。十嘿。因惭。
for(var i=0;i<5;i++){
setTimeout(function () {
console.log(i);
},i*1000);
}
很明顯,上面代碼每隔1秒輸出一個(gè)5绩衷”哪В可能在面試的時(shí)候激率,面試官會(huì)要求寫(xiě)出一個(gè)代碼,從1開(kāi)始版姑,每隔1秒輸出2柱搜,3迟郎,一直到5剥险。或者怎樣宪肖,哈哈表制,反正就是類似的吧。
大家對(duì)于上面這個(gè)代碼估計(jì)也已經(jīng)爛熟了控乾,肯定不會(huì)這么寫(xiě)么介。可是蜕衡,大家有沒(méi)有想過(guò)為什么結(jié)果是這樣呢壤短?有的人可能會(huì)說(shuō) ,很明顯嘛慨仿,我們?cè)O(shè)置了5個(gè)定時(shí)器久脯,但這5個(gè)定時(shí)器里函數(shù)是異步執(zhí)行的,當(dāng)for循環(huán)結(jié)束時(shí)镰吆,i是5帘撰,所以輸出的都是5。
這說(shuō)法吧万皿,對(duì)摧找,但沒(méi)說(shuō)到根上,因?yàn)槟乩喂瑁琫s6添加了個(gè)let就不這樣:
for(let i=0;i<5;i++){
setTimeout(function () {
console.log(i);
},i*1000);
}
這個(gè)代碼蹬耘,輸出的就是每隔1秒輸出一個(gè)i,而不是5個(gè)5减余。
為什么综苔?我們就說(shuō)說(shuō)這個(gè)問(wèn)題的根源。
先說(shuō)var這個(gè)佳励。
我們傳遞給setTimeout()的函數(shù)休里,形成一個(gè)閉包,我們總共設(shè)置了5個(gè)setTimeout()共形成5個(gè)閉包赃承,這5個(gè)閉包共享一個(gè)全局的詞法作用域妙黍,因此共享一個(gè)i。當(dāng)循環(huán)結(jié)束后瞧剖,實(shí)際上傳遞給這5個(gè)定時(shí)器的i是同一個(gè)拭嫁,都是5可免。
而對(duì)于let,由于let的塊級(jí)作用域做粤,for循環(huán)頭部的let不僅將i綁定到了for循環(huán)的塊中浇借,事實(shí)上它將其重新綁定到了循環(huán)的每一個(gè)迭代中,確保使用上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值怕品。也就是說(shuō)妇垢,對(duì)于這5個(gè)閉包而言,每次i都是重新賦值肉康,因此不會(huì)存在var上面的問(wèn)題闯估。
關(guān)于let和var的具體區(qū)別,請(qǐng)參考:深入淺出ES6(十四):let和const
對(duì)吼和,這里的根就和閉包有關(guān)系涨薪。
在let之前,可能有的人會(huì)提出如下的方案:
for(var i=0;i<5;i++){
(function (i) {
setTimeout(function () {
console.log(i);
},i*1000);
})(i)
}
這里我們就是用的閉包的作用解決的var的問(wèn)題炫乓。我們用自執(zhí)行函數(shù)IIFE為每個(gè)循環(huán)生成一個(gè)新的作用域刚夺,每個(gè)循環(huán)的setTimeout()內(nèi)的函數(shù)的作用域封閉在每個(gè)循環(huán)內(nèi)部。也就是末捣,我們?yōu)槊總€(gè)循環(huán)生成了一個(gè)新的塊級(jí)作用域侠姑,這樣使得每次循環(huán)都能取得正確的值。let就是這個(gè)原理塔粒,只不過(guò)是簡(jiǎn)化了代碼而已结借。
可能有的人還在迷糊,這里有兩層函數(shù)卒茬,哪個(gè)是閉包呢船老?
為方便分析,我們給函數(shù)加上標(biāo)識(shí)符:
for(var i=0;i<5;i++){
(function iife(i) {
setTimeout(function timer() {
console.log(i);
},i*1000);
})(i)
}
每次循環(huán)的自執(zhí)行函數(shù)圃酵,我們命名為iife()柳畔,定時(shí)器中的函數(shù)我們命名為timer()。這里的閉包是timer()函數(shù)形成了對(duì)iife()函數(shù)作用域的閉包郭赐。
我們說(shuō)過(guò)薪韩,要分析閉包,就要搞清楚兩個(gè)關(guān)鍵點(diǎn):函數(shù)捌锭,和其所在的作用域俘陷。
函數(shù),是timer()函數(shù)观谦,其所在的作用域就是iife()函數(shù)作用域拉盾。當(dāng)函數(shù)iife()函數(shù)執(zhí)行完,定義完成立即執(zhí)行(自執(zhí)行)豁状,其每個(gè)timer()函數(shù)在定時(shí)器內(nèi)還是能夠訪問(wèn)其作用域內(nèi)的變量捉偏,因此倒得,timer()就是閉包,對(duì)每個(gè)iiff()函數(shù)作用域的閉包夭禽。
本質(zhì)上無(wú)論何時(shí)何地霞掺,如果將函數(shù)(訪問(wèn)它們各自的詞法作用域)當(dāng)作第一級(jí)的值類型并到處傳遞,你就會(huì)看到閉包在這些函數(shù)中的應(yīng)用讹躯。在定時(shí)器菩彬、事件監(jiān)聽(tīng)器、Ajax請(qǐng)求蜀撑、跨窗口通信挤巡、Web Workers或者任何其他的異步(或者同步)任務(wù)中,只要使用了回調(diào)函數(shù)酷麦,實(shí)際上就是在使用閉包!
摘錄來(lái)自: Kyle Simpson喉恋、趙望野沃饶、梁杰. “你不知道的JavaScript(上卷)”。 iBooks.
再來(lái)一個(gè)時(shí)間的例子:
function process(data){
//做一些有趣的事
}
var someReallyBigData = {..};
process(someReallyBigData);
var btn = document.getElementById('mybtn')
btn.addEventListener('click',function click(evt){
console.log('button clicked');
})
click點(diǎn)擊函數(shù)并不需要someReallyBigData變量轻黑,當(dāng)process()執(zhí)行完后糊肤,在內(nèi)存中占用大量空間的數(shù)據(jù)結(jié)構(gòu)就可以被垃圾回收了。但是氓鄙,由于click函數(shù)形了一個(gè)覆蓋整個(gè)作用域的閉包馆揉,javascript引擎極有可能依然保存著這個(gè)結(jié)構(gòu)(這取決于具體實(shí)現(xiàn))。
上面這個(gè)例子是《你不知道的js》中解釋塊級(jí)作用域的一個(gè)例子抖拦。當(dāng)然放在這里用說(shuō)明對(duì)于事件監(jiān)聽(tīng)形成的閉包的一個(gè)簡(jiǎn)單例子升酣。
小結(jié)
閉包是什么,可以直接用MDN上對(duì)于閉包的說(shuō)明進(jìn)行回答态罪。但是噩茄,如要想要真正理解閉包,請(qǐng)從兩個(gè)關(guān)鍵點(diǎn)去分析:函數(shù)复颈,和其創(chuàng)建時(shí)所在的詞法作用域绩聘。
函數(shù)能夠記住其所在的詞法作用域,即使在其作用域之外執(zhí)行耗啦,這就形成了閉包凿菩。
作為一個(gè)合格的面試官,請(qǐng)不要直接問(wèn)“什么是閉包”這種問(wèn)題了帜讲,估計(jì)沒(méi)有人都說(shuō)清楚衅谷。要想考察對(duì)于閉包的理解,可以模擬幾個(gè)用閉包解決的場(chǎng)景來(lái)考察舒帮,比如上面的幾個(gè)例子会喝。