如何更容易理解js中的閉包

閉包是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è)例子会喝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末陡叠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子肢执,更是在濱河造成了極大的恐慌枉阵,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件预茄,死亡現(xiàn)場(chǎng)離奇詭異兴溜,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)耻陕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)拙徽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人诗宣,你說(shuō)我怎么就攤上這事膘怕。” “怎么了召庞?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵岛心,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我篮灼,道長(zhǎng)忘古,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任诅诱,我火速辦了婚禮髓堪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘娘荡。我一直安慰自己干旁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布它改。 她就那樣靜靜地躺著疤孕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪央拖。 梳的紋絲不亂的頭發(fā)上祭阀,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音鲜戒,去河邊找鬼专控。 笑死,一個(gè)胖子當(dāng)著我的面吹牛遏餐,可吹牛的內(nèi)容都是我干的伦腐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼失都,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼柏蘑!你這毒婦竟也來(lái)了幸冻?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤咳焚,失蹤者是張志新(化名)和其女友劉穎洽损,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體革半,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碑定,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了又官。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片延刘。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖六敬,靈堂內(nèi)的尸體忽然破棺而出碘赖,到底是詐尸還是另有隱情,我是刑警寧澤觉阅,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布歧蒋,位于F島的核電站晌砾,受9級(jí)特大地震影響涧窒,放射性物質(zhì)發(fā)生泄漏蹭睡。R本人自食惡果不足惜胃夏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一扑眉、第九天 我趴在偏房一處隱蔽的房頂上張望昼捍。 院中可真熱鬧翠肘,春花似錦眯亦、人聲如沸伤溉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)乱顾。三九已至,卻和暖如春宫静,著一層夾襖步出監(jiān)牢的瞬間走净,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工孤里, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留伏伯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓捌袜,卻偏偏與公主長(zhǎng)得像说搅,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子虏等,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容