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