一文了解你不知道的JavaScript閉包篇
前言
JavaScript語(yǔ)言中有一個(gè)非常重要又難以掌握,近似神話的概念-閉包。對(duì)于有一點(diǎn)JavaScript使用經(jīng)驗(yàn)但從未真正理解閉包概念的人來(lái)說(shuō),理解閉包可以看作是某種意義上的重生。JavaScript中閉包無(wú)處不在,我們只需要能夠識(shí)別并擁抱它。它是基于詞法作用域書(shū)寫(xiě)代碼時(shí)所產(chǎn)生的自然結(jié)果,在代碼中隨處可見(jiàn)。
理解閉包
下面用一些代碼來(lái)解釋這個(gè)定義:
function foo(){ var a = 2; function bar(){ console.log(a);//2 } bar(); } foo()
這是閉包嗎?也許是的,但似乎這種方式對(duì)必報(bào)的定義并不能直接進(jìn)行觀察,也無(wú)法明白這個(gè)代碼片段中閉包是如何工作的。我們很容易地理解詞法作用域,而閉包則隱藏在代碼之后的神秘陰影里,并不那么容易理解。
下面我們來(lái)看一段代碼,清晰的展示了閉包:
function foo(){ var a = 2; function bar(){ console.log(a) } return bar; } var baz = foo(); baz() //2-------這就是閉包的效果。
函數(shù)bar的詞法作用域能夠訪問(wèn)foo()的內(nèi)部作用域。然后我們將bar()函數(shù)本身當(dāng)作一個(gè)值類型進(jìn)行傳遞。在這個(gè)例子中,我們將bar所引用的函數(shù)對(duì)象本身當(dāng)作返回值。在foo()執(zhí)行后,它的返回值(bar函數(shù))賦值給變量baz并調(diào)用baz(),實(shí)際上只是通過(guò)不同的標(biāo)識(shí)符引用調(diào)用了內(nèi)部的函數(shù)baz().foo內(nèi)部的bar()顯示可以被正常執(zhí)行。
在foo()執(zhí)行后,通常會(huì)期待foo()的整個(gè)內(nèi)部作用域都被銷毀,因?yàn)槲覀冎酪嬗欣厥掌鱽?lái)釋放不再使用的空間。由于foo()似乎不會(huì)在被利用,所以大腦很自然的認(rèn)為會(huì)對(duì)其進(jìn)行回收。
而閉包的神奇之處正是可以阻止這件事的發(fā)生。事實(shí)上內(nèi)部作用域依然存在,因此沒(méi)有被回收。誰(shuí)在使用這個(gè)內(nèi)部作用域呢?原來(lái)是bar()本身在使用。所以拜bar()所聲明的位置所賜,它擁有覆蓋foo()內(nèi)部作用域的閉包,使得foo()的作用域能夠一直存活,以供bar()在之后任何時(shí)間都可以被調(diào)用。
bar()依然持有對(duì)該作用域的引用,而這個(gè)引用就叫做閉包。
當(dāng)然,傳遞函數(shù)也是可以間接的:
var fn; function foo(){ var a = 2; function baz(){ console.log(a); } fn = baz; } function bar(){ fn() } foo(); bar(); //2-------這就是閉包!
無(wú)論通過(guò)何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域之外,它都會(huì)持有原始定義作用域的引用,無(wú)法在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包。
升級(jí)版閉包
前面的代碼片段可能有些死板,并且為了解釋如何使用閉包而把代碼寫(xiě)的很明顯。但其實(shí)閉包在實(shí)際操作中是個(gè)很好玩的工具,而且大家也一定都用過(guò)閉包。現(xiàn)在讓我們來(lái)搞懂這個(gè)事實(shí):
function wait(message){ setTimeout(function timer(){ console.log(message); },1000); } wait ("Hello closure")
將一個(gè)內(nèi)部函數(shù)(名為timer)傳遞給setTimeout().timer具有涵蓋wait()作用域的閉包,因此還保有對(duì)變量message的引用。
wait(...)執(zhí)行1000毫秒,它的內(nèi)部作用域并不會(huì)消失,timer函數(shù)依然保有wait()作用域的閉包。這就是閉包。我不知道你在生活中都會(huì)寫(xiě)什么樣的代碼,但在定時(shí)器、事件監(jiān)聽(tīng)器、Ajax請(qǐng)求、跨窗口通信或者任何其他的異步任務(wù),只要你使用了回調(diào)函數(shù),實(shí)際上就是在使用閉包。
var a = 2; ( function IIFE(){ console.log(a) } )()
雖然這段代碼可以正常工作,但嚴(yán)格來(lái)講它并不是什么閉包,因?yàn)楹瘮?shù)并不是在它本身的詞法作用域以外執(zhí)行的。它在定義時(shí)所在的作用域中執(zhí)行(而外部作用域,也就是全局作用域也持有a)。a是通過(guò)普通的詞法作用域查找而非閉包被發(fā)現(xiàn)的。
循環(huán)和閉包
要說(shuō)明閉包,循環(huán)for是最常見(jiàn)的例子。
for(var i = 1;i<=5;i++){ setTimeout(function timer(){ console.log(i) },i*1000) }
正常情況下,我們對(duì)這段代碼的行為預(yù)期是分別輸出數(shù)字1~5,每秒一次,每次一個(gè)。
但實(shí)際上,這段代碼在運(yùn)行時(shí)會(huì)以每秒一次的頻率輸出五次6。
這是為什么?
首先解釋6是從哪里來(lái)的。這個(gè)循環(huán)的終止條件是i不再小于等于5.條件首次成立時(shí)i的值是6.因此,輸出顯示的是循環(huán)結(jié)束時(shí)i的最終值。
其次要清楚,延遲函數(shù)的回調(diào)會(huì)在循環(huán)結(jié)束才執(zhí)行。事實(shí)上,當(dāng)給定時(shí)器運(yùn)行時(shí)即使每個(gè)迭代器中執(zhí)行的是setTimeout(..,0),所有的回調(diào)函數(shù)依然是在循環(huán)結(jié)束后才會(huì)被執(zhí)行,因此會(huì)每次輸出一個(gè)6出來(lái)。
那為什么沒(méi)有獲得我們預(yù)期的結(jié)果呢,從1~5輸出?
根據(jù)作用域原理,盡管循環(huán)中五個(gè)輸出函數(shù)是被分別定義出來(lái)的,但是他們都被封閉在一個(gè)共享全局作用域下,實(shí)際上還是共享一個(gè)i。
那好,知道解決方法咯,那就不共享一個(gè)i,讓每一次的值都相互獨(dú)立,每次都獲得對(duì)應(yīng)的值。
for(var i = 1;i<=5;i++){ ( function(){ var j = i; setTimeout(function timer(){ console.log(j) },j*1000) } )() }
這樣,將每一次的i值都傳給另一個(gè)變量,保證i的實(shí)時(shí)更新,就可以正常輸出1~5了!
知道原因了,舉一反三,那是不是也會(huì)有其他解決辦法呢?
仔細(xì)思考我們前面的解決方案。我們使用IIFE函數(shù)(立即執(zhí)行函數(shù))每次迭代時(shí)都創(chuàng)建了一個(gè)新的作用域。換句話說(shuō),我們每次迭代都產(chǎn)生新的塊作用域。那是不是可以聲明塊作用域避免變量共享的問(wèn)題呢?
for(let i = 1;i<=5;i++){ setTimeout(function timer(){ console.log(i) },i*1000) }
很酷是吧?塊作用域和閉包聯(lián)手便可“天下無(wú)敵”。
模塊
function CoolModule(){ var something = "cool"; var another = [1,2,3]; function doSomething(){ console.log(something); } function doAnother(){ console.log(another.join("!")); } return { doSomething:doSomething, doAnother:doAnother } } var foo = CoolModule(); foo.doSomething();//cool foo.doAnother();//1!2!3
這個(gè)模式在JavaScript中被稱為模塊,最常見(jiàn)的實(shí)現(xiàn)模塊模式的方法通常被稱為模塊暴露。首先,CoolModule()只是一個(gè)函數(shù),必須要通過(guò)它來(lái)創(chuàng)建一個(gè)模塊實(shí)例。如果不執(zhí)行外部函數(shù),內(nèi)部作用域和閉包都無(wú)法被創(chuàng)建。
其次,CoolModule()返回一個(gè)用對(duì)象字面量語(yǔ)法{key:value,...}來(lái)表示的對(duì)象。這個(gè)返回的對(duì)象中含有對(duì)內(nèi)部對(duì)象而不是內(nèi)部數(shù)據(jù)變量的引用。我們保持內(nèi)部數(shù)據(jù)變量是隱藏且私有的狀態(tài)??梢詫⑦@個(gè)對(duì)象類型的返回值看作本質(zhì)上模塊的公共API。
doSomething()和doAnother()函數(shù)具有涵蓋模塊實(shí)例內(nèi)部作用域的閉包(通過(guò)調(diào)用CoolModule()實(shí)現(xiàn))。
簡(jiǎn)單描述一下,模塊模式的閉包需要具備兩個(gè)必要條件。
(1)必須有外部的封閉函數(shù),該函數(shù)必須至少被調(diào)用一次。(每次調(diào)用都會(huì)創(chuàng)建一個(gè)新的模塊實(shí)例)。
(2)封閉函數(shù)必須返回至少一個(gè)內(nèi)部函數(shù),這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問(wèn)或者修改私有的狀態(tài)。
另外,模塊也是普通的函數(shù),因此可以接受參數(shù):
function CoolModule(id){ function identify(){ console.log(id) } return { identify:identify } } var foo1 = CoolModule("foo1"); var foo2 = CoolModule("foo2"); foo1.identify();//"foo1" foo2.identify();//"foo2"
小結(jié)
閉包就好像從JavaScript中分離出來(lái)的一個(gè)充滿神秘色彩的未開(kāi)化世界,只有最勇敢的人才能夠到達(dá)那里,但實(shí)際上他只是一個(gè)標(biāo)準(zhǔn),顯然就是關(guān)于如何在函數(shù)作為值按需傳遞的此法環(huán)境中書(shū)寫(xiě)代碼的。
當(dāng)函數(shù)可以記住并訪問(wèn)所在的詞法作用域,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行,這就產(chǎn)生了閉包。
如果沒(méi)能認(rèn)出閉包,也不了解它的工作原理,在使用它的過(guò)程中就很容易犯錯(cuò),比如在循環(huán)中。但同時(shí)閉包也是一個(gè)非常強(qiáng)大的工具,可以用多種形式來(lái)實(shí)現(xiàn)模塊等模式。
模塊主要有兩個(gè)特征:
(1)為創(chuàng)建內(nèi)部工作域而調(diào)用了一個(gè)包含函數(shù)。
(2)包裝函數(shù)的返回值必須至少包括一個(gè)對(duì)內(nèi)部函數(shù)的引用,這樣就會(huì)創(chuàng)建涵蓋整個(gè)包裝函數(shù)內(nèi)部作用域的閉包。
以上就是一文了解你不知道的JavaScript閉包篇的詳細(xì)內(nèi)容,更多關(guān)于JavaScript 閉包的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序?qū)崿F(xiàn)一個(gè)簡(jiǎn)單swiper代碼實(shí)例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)一個(gè)簡(jiǎn)單swiper代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12純?JS?實(shí)現(xiàn)的輕量化圖片編輯器實(shí)例詳解
這篇文章主要為大家介紹了純JS實(shí)現(xiàn)的輕量化圖片編輯器實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10解決layer.confirm選擇完之后消息框不消失的問(wèn)題
今天小編就為大家分享一篇解決layer.confirm選擇完之后消息框不消失的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09使用OpenLayers3 添加地圖鼠標(biāo)右鍵菜單
這篇文章主要介紹了使用OpenLayers3 添加地圖鼠標(biāo)右鍵菜單的相關(guān)資料,需要的朋友可以參考下2015-12-12uniapp實(shí)現(xiàn)橫向滾動(dòng)選擇日期
這篇文章主要為大家詳細(xì)介紹了uniapp實(shí)現(xiàn)橫向滾動(dòng)選擇日期,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10前端學(xué)習(xí)筆記style,currentStyle,getComputedStyle的用法與區(qū)別
這篇文章主要介紹了前端學(xué)習(xí)筆記style,currentStyle,getComputedStyle的用法與區(qū)別,需要的朋友可以參考下2016-05-05解決LayUI加上form.render()下拉框和單選以及復(fù)選框不出來(lái)的問(wèn)題
今天小編就為大家分享一篇解決LayUI加上form.render()下拉框和單選以及復(fù)選框不出來(lái)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09