摘要:由于聲明在函數(shù)內(nèi)部,所以它擁有涵蓋內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以便在以后的任何時間進行引用。盡管本身并不是觀察閉包的恰當(dāng)例子,但他的確創(chuàng)建了一個封閉的作用域,并且也是最常用來創(chuàng)建被封閉起來的閉包的工具。
在講解作用域閉包的內(nèi)容之前,需要對以下概念有所掌握:
JavaScript具有兩種作用域:全局作用域和函數(shù)作用域,至于塊作用域也不能說沒有,比如說: try ...catch...語句中,catch分句就是塊作用域,還有with語句等。
ES6中的let關(guān)鍵字,可以用來在任意代碼塊中聲明變量。
什么事立即執(zhí)行函數(shù)表達式以及它的作用。
老生常談什么是閉包閉包的概念:函數(shù)可以記住并訪問所在的詞法作用域時,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行,這時就產(chǎn)生了閉包。
function foo(){ var a = 2; function bar(){ console.log(a); } return bar; } var baz = foo(); baz(); //這就是閉包的效果
函數(shù)bar()的詞法作用域能夠訪問foo()的內(nèi)部作用域,然后我們將bar()函數(shù)本身當(dāng)作一個值進行傳遞。在foo()執(zhí)行后,其返回值賦值給變量baz并調(diào)用baz()。
在foo()執(zhí)行后,通常會期待foo()的整個內(nèi)部作用于都被銷毀,因為我們知道引擎有垃圾回收機制來釋放不在使用的內(nèi)存空間。由于看上去foo()的內(nèi)容不會再被使用,所以很自然地會考慮對其進行回收。
但是,閉包的神奇之處在于可以阻止這件事情發(fā)生。事實上內(nèi)部作用域依然存在,因此,沒有被回收。那么是誰在使用這個內(nèi)部作用域呢?當(dāng)然是bar()在使用。
由于bar()聲明在foo()函數(shù)內(nèi)部,所以它擁有涵蓋foo()內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以便bar()在以后的任何時間進行引用。
bar()函數(shù)在foo()調(diào)用完成后,依舊持有對其作用域的引用,而這個引用就叫做閉包
當(dāng)然,無論使用何種方式對函數(shù)類型的值進行傳遞,當(dāng)函數(shù)在別處調(diào)用時都可以觀察到閉包
function foo(){ var a = 2; function baz(){ console.log(a)//2 } bar(baz); } function bar(fn){ fn(); //這就是閉包 }
相比于上面代碼的枯燥,這有一個更加常見的例子
function wait(message){ setTimeout(function time(){ console.log(message); }, 1000); } wait("hello clousre");
簡單分析一下這段代碼:我們將一個名為time的內(nèi)部函數(shù)傳遞給setTimeout(),time具有涵蓋wait()作用域的閉包,因此,還保有對變量message的引用。
wait(..)執(zhí)行1000ms后,它的內(nèi)部作用域并不會消失,time()函數(shù)依舊保有對wait()作用域的閉包,在引擎內(nèi)部,內(nèi)置的工具函數(shù)setTimeout()會持有一個對參數(shù)的引用,這個參數(shù)也許叫作fn或者func之類的。引擎會調(diào)用這個函數(shù),而詞法作用域在這個過程中保持完整。
這就是閉包
那么閉包有哪些應(yīng)用呢?其實包括定時器,事件監(jiān)聽器,Ajax請求,跨窗口通信,Web Workers或者任何其他的異步(或者同步)任務(wù)中,只要使用回掉函數(shù),實際上就是在使用閉包!
這里我們再看一個特別典型閉包的例子,但嚴(yán)格來說它并不是閉包。
var a = 2; (function IIFE(){ console.log(a) })();
IIFE即立即執(zhí)行函數(shù)表達式,第一個()讓函數(shù)變?yōu)楹瘮?shù)表達式,第二個()函數(shù)執(zhí)行。為什么說他嚴(yán)格上來講并不是閉包呢?因為在示例代碼中函數(shù)并不是在它本身的詞法作用域之外執(zhí)行的,它在其定義時所在的作用域執(zhí)行,a是通過詞法作用域查找到的,并不是閉包發(fā)現(xiàn)的。
盡管IIFE本身并不是觀察閉包的恰當(dāng)例子,但他的確創(chuàng)建了一個封閉的作用域,并且也是最常用來創(chuàng)建被封閉起來的閉包的工具。
說到閉包我們接觸最早的也許就是for循環(huán)的例子:
for(var i = 1; i<6; i++){ setTImeout(function time(){ console.log(i) }, i*1000) }
記得第一次看見這段代碼的時候,那是被深深的虐到,作為C語言起手的同學(xué),當(dāng)時真的是一臉的懵逼,為什么會輸出5個6, 為什么會輸出5個6,為什么?當(dāng)時其他人的講解也是模模糊糊的,雖然提出了解決方法,當(dāng)還是無法理解這其中的機制原理,所以,我痛下決心把它弄懂!也許只有我不懂吧!
問:為什么會輸出66666呢?
答:能輸出66666說明for循環(huán)內(nèi)部的代碼的確執(zhí)行了5次。
問:那6是從哪來的呢?
答:6是我們循環(huán)的終止條件,所以輸出6。
問:那為什么不是循環(huán)一次,輸出一個值, 1,2,3,4,5這樣呢?
答:setTimeout()函數(shù)是在循環(huán)結(jié)束時執(zhí)行的,就算是你設(shè)置setTimeout(fn, 0),它也是在for循環(huán)完成后立即執(zhí)行,總之就是在for循環(huán)執(zhí)行完成后才執(zhí)行。
好了,這就不難理解了為什么會輸出66666了。但這也就引出了一個更深入的話題,代碼中到底什么缺陷導(dǎo)致它的行為同語義暗示的不一致呢?
缺陷是:我們試圖假設(shè)循環(huán)中的每個迭代在運行時都會給自己“捕獲”一個i的副本。但是根據(jù)作用域的工作原理,實際情況是盡管循環(huán)中的五個函數(shù)是在各個迭代中分別定義的,但是它們都被封閉在一個共享的全局作用域中,因此實際上只有一個i。所以,實際的樣子是這樣。
而我們想象中的樣子確是這樣。
下面回到正題。既然明白了缺陷是什么,那么要怎樣做才能達到我們想象中的樣子呢?答案是我們需要在每一次迭代的過程中都創(chuàng)建一個閉包作用域。在上文中我們已經(jīng)有所鋪墊,IIFE會通過聲明立即執(zhí)行一個函數(shù)來創(chuàng)建作用域。so我們可以將代碼改成下面的樣子:
for(var i=1; i<6; i++){ (function(){ setTImeout(function time(){ console.log(i) }, i*1000) })(); }
這樣每一次迭代我們都創(chuàng)建了一個封閉的作用域(你可以想象為上圖中黃色的矩形部分)。但是這樣做仍舊不行,為什么呢?因為雖然每個延遲函數(shù)都會將IIFE在每次迭代中創(chuàng)建的作用域封閉起來,但我們封閉的作用域是空的,所以必須傳點東西過去才能實現(xiàn)我們想要的結(jié)果。
for(var i=1; i<6; i++){ (function(){ var j = i setTImeout(function time(){ console.log(j) }, j*1000) })(); }
ok!試試現(xiàn)在他能正常工作嗎?對這段代碼再進行一點改進
for(var i=1; i<6; i++){ (function(j){ setTImeout(function time(){ console.log(j) }, j*1000) })(i); }
總的來說,就是在迭代內(nèi)使用IIFE會為每個迭代都生成一個新的作用域,使得延遲函數(shù)可以將新的作用域封閉在每個迭代內(nèi)部,我們同時在迭代的過程中將每次迭代的i值作為參數(shù)傳入進新的作用域,這樣在迭代中創(chuàng)建的封閉作用域就都會含有一個具有正確值的變量供我們訪問。ok,it"s work!
塊作用域仔細思考我們前面的解決方案。我們使用IIFE在每次迭代時都創(chuàng)建一個新的作用域。也就是說,每次迭代我們都需要一個塊作用域。前面我們提到,你需要對ES6中的let關(guān)鍵字進行了解,它可以用來劫持塊作用域,并且在這個塊作用域中聲明一個變量。
本質(zhì)上來講它是將一個塊轉(zhuǎn)換成可以被關(guān)閉的作用域。
for(var i=1; i<6; i++){ let j = i; //閉包的塊作用域 setTImeout(function time(){ console.log(j) }, j*1000) }
如果將let聲明在for循環(huán)的頭部那么將會有一些特殊的行為,有多特殊呢?它會指出變量在循環(huán)過程中不止被聲明一次,每次迭代都會聲明。隨后的每個迭代都會使用上一個迭代結(jié)束時的值來初始化這個變量。不管這句話有多拗口,看看代碼吧!
for(let i=1; i<6; i++){ setTImeout(function time(){ console.log(i) }, i*1000) }
有沒有似曾相識的感覺,有沒有感動到,我已經(jīng)老淚縱橫了。。。
下一節(jié)講閉包運用--模塊機制
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/79010.html
摘要:一言以蔽之,閉包,你就得掌握。當(dāng)函數(shù)記住并訪問所在的詞法作用域,閉包就產(chǎn)生了。所以閉包才會得以實現(xiàn)。從技術(shù)上講,這就是閉包。執(zhí)行后,他的內(nèi)部作用域并不會消失,函數(shù)依然保持有作用域的閉包。 網(wǎng)上總結(jié)閉包的文章已經(jīng)爛大街了,不敢說筆者這篇文章多么多么xxx,只是個人理解總結(jié)。各位看官瞅瞅就好,大神還希望多多指正。此篇文章總結(jié)與《JavaScript忍者秘籍》 《你不知道的JavaScri...
摘要:也正因為這個閉包的特性,閉包函數(shù)可以讓父函數(shù)的數(shù)據(jù)一直駐留在內(nèi)存中保存,從而這也是后來模塊化的基礎(chǔ)。只有閉包函數(shù),可以讓它的父函數(shù)作用域永恒,像全局作用域,一直在內(nèi)存中存在。的本質(zhì)就是如此,每個模塊文件就是一個大閉包。 為什么會有閉包 js之所以會有閉包,是因為js不同于其他規(guī)范的語言,js允許一個函數(shù)中再嵌套子函數(shù),正是因為這種允許函數(shù)嵌套,導(dǎo)致js出現(xiàn)了所謂閉包。 function...
摘要:曾幾何時,閉包好像就是一個十分難以捉摸透的東西,看了很多文章,對閉包都各有說法,以致讓我十分暈,什么內(nèi)部變量外部變量的,而且大多數(shù)都只描述一個過程,沒有給閉包的定義,最后,舉幾個例子,告訴你這就是閉包。 曾幾何時,閉包好像就是一個十分難以捉摸透的東西,看了很多文章,對閉包都各有說法,以致讓我十分暈,什么內(nèi)部變量、外部變量的,而且大多數(shù)都只描述一個過程,沒有給閉包的定義,最后,舉幾個例子...
摘要:內(nèi)部的稱為內(nèi)部函數(shù)或閉包函數(shù)。過度使用閉包會導(dǎo)致性能下降。,閉包函數(shù)分為定義時,和運行時。循環(huán)會先運行完畢,此時,閉包函數(shù)并沒有運行。閉包只能取得外部函數(shù)中的最后一個值。事件綁定種的匿名函數(shù)也是閉包函數(shù)。而對象中的閉包函數(shù),指向。 閉包概念解釋: 閉包(也叫詞法閉包或者函數(shù)閉包)。 在一個函數(shù)parent內(nèi)聲明另一個函數(shù)child,形成了嵌套。函數(shù)child使用了函數(shù)parent的參數(shù)...
摘要:所以,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實體。所以本文中將以維基百科中的定義為準(zhǔn)即在計算機科學(xué)中,閉包,又稱詞法閉包或函數(shù)閉包,是引用了自由變量的函數(shù)。 閉包(closure)是JavaScript中一個神秘的概念,許多人都對它難以理解,我也一直處于似懂非懂的狀態(tài),前幾天深入了解了一下執(zhí)行環(huán)境以及作用域鏈,可戳查看詳情,而閉包與作用域及作用域鏈的關(guān)系密不可分,所...
閱讀 1165·2021-11-25 09:43
閱讀 2976·2019-08-30 15:54
閱讀 3363·2019-08-30 15:54
閱讀 3010·2019-08-30 15:44
閱讀 1636·2019-08-26 12:18
閱讀 2266·2019-08-26 11:42
閱讀 887·2019-08-26 11:35
閱讀 3306·2019-08-23 18:22