閉包
說(shuō)到作用域閉包未檩,我想很多同學(xué)都知道戴尸,但是讓你講講其原理以及應(yīng)用場(chǎng)景,也許又不知從何說(shuō)起冤狡。
其實(shí)作用域閉包無(wú)處不在孙蒙,只是你自己沒(méi)意識(shí)到。
簡(jiǎn)單來(lái)說(shuō)悲雳,函數(shù)能夠記住并可以訪問(wèn)所在的詞法作用域時(shí)挎峦,便產(chǎn)生了閉包。看過(guò)<<JavaScript 高級(jí)程序設(shè)計(jì)>>的同學(xué)也可能會(huì)這樣說(shuō)合瓢,閉包就是定義在函數(shù)里的函數(shù)嘍坦胶。其實(shí)這兩種解釋是一個(gè)意思,為什么這么說(shuō)呢晴楔,我們先來(lái)看段代碼
function foo(){
? ? ? ? var a = 2;
? ? ? ? function bar(){
? ? ? ? ? ? ? console.log(a);
? ? ? ? }
? ? ? ? return bar;
}
var test = foo();
test();//輸出結(jié)果為2
看完這段代碼你有發(fā)現(xiàn)什么問(wèn)題嗎顿苇?函數(shù)bar竟然在其所在的作用域外部被調(diào)用了。按道理來(lái)說(shuō)税弃,bar涵蓋了foo定義的作用域岖圈,也只能在函數(shù)內(nèi)部被調(diào)用。之所以在外部能被調(diào)用钙皮,是因?yàn)槭紫葘ar標(biāo)識(shí)符當(dāng)做變量被foo返回蜂科,當(dāng) foo() 執(zhí)行后,bar便賦值給了test短条,test的調(diào)用就是對(duì)bar的調(diào)用导匣,這個(gè)過(guò)程實(shí)質(zhì)上就是通過(guò)不同的標(biāo)識(shí)符引用調(diào)用了內(nèi)部的bar。
當(dāng) foo() 執(zhí)行后茸时,理論上內(nèi)部的作用域是要被銷(xiāo)毀的贡定,因?yàn)橐嬗欣厥掌魍ㄟ^(guò)標(biāo)記清除來(lái)回收不再使用的內(nèi)存空間。但事實(shí)上可都,foo的內(nèi)部作用域一直存在缓待,被誰(shuí)占用著呢?就是這個(gè)內(nèi)部函數(shù)bar()渠牲,拜它聲明的位置所賜旋炒,它依然保持著對(duì)foo內(nèi)部作用域的引用,這個(gè)引用其實(shí)就是閉包签杈。因此瘫镇,后續(xù)的 test 才能夠執(zhí)行,并能夠訪問(wèn)其所在作用域中的變量a。
其實(shí)說(shuō)白了铣除,如果將函數(shù)作為參數(shù)并到處傳遞谚咬,其所涵蓋的作用域(例如其所在的函數(shù)構(gòu)建的作用域)就一直在那里。所以像定時(shí)器尚粘、事件監(jiān)聽(tīng)器择卦、Ajax請(qǐng)求等,只要應(yīng)用了回調(diào)函數(shù)的地方郎嫁,我們都可以認(rèn)為是在使用閉包互捌。
循環(huán)和閉包
說(shuō)到閉包,另一個(gè)典型的例子就是for循環(huán)了行剂,看下面這段代碼:
for( var i = 1; i <= 5; i ++){
? ? ? setTimeout(function(){
? ? ? ? ? ? console.log(i);
? ? ? ?},i*1000);
}
上面代碼會(huì)輸出什么結(jié)果呢?是 1 2 3 4 5嗎钳降?不是的厚宰,其實(shí)是以每1秒的頻率輸出5次6,這是因?yàn)榈谝凰焯睢⒀舆t函數(shù) setTimeout 里面定義的回調(diào)函數(shù)必須等到循環(huán)結(jié)束時(shí)才會(huì)調(diào)用铲觉,這個(gè)時(shí)候 i 已經(jīng)變成6,;第二吓坚、每一次循環(huán)都會(huì)定義一個(gè)延遲函數(shù)撵幽,這樣的話回調(diào)函數(shù)要調(diào)用5次。
不過(guò)礁击,我們的真實(shí)的目的是想按順序輸出數(shù)字對(duì)吧盐杂,怎么辦呢?有人可能很快想到了用作用域包起來(lái)啊哆窿,聰明的你怎么寫(xiě)代碼呢链烈?
for( var i = 1; i <= 5; i++){
? ?(function(){
? ? ? ? ? ?var j = i;
? ? ? ? ?? setTimeout(function(){
? ? ? ? ? ? console.log(i);
? ? ? ? ? },i*1000);
? ? })();
}
僅僅是包起來(lái)還不夠,還要添加點(diǎn)代碼挚躯。在每個(gè)作用域內(nèi)部定義一個(gè)屬于自己的變量 j强衡,這樣,任你外部的 i 再怎么變化码荔,我已經(jīng)有了自己的專(zhuān)屬變量 j漩勤,這樣就可以達(dá)到目的了。
當(dāng)然缩搅,代碼還可以改成下面這樣:
for( var i = 1; i <= 5; i ++){
? ?(function(){
? ? ? ? ?? setTimeout(function(j){
? ? ? ? ? ? console.log(i);
? ? ? ? ? },i*1000);
? ? })(i);
}
回到塊作用域
我前面的文章有提到過(guò)塊作用域越败,其中ES6新引入的 let 就可以劫持塊作用域,并在這個(gè)作用域中聲明一個(gè)變量硼瓣。本質(zhì)上就是將塊作用域轉(zhuǎn)化成封閉的作用域了眉尸。因此,我們可以寫(xiě)出下面的代碼,照樣能夠按順序輸出數(shù)字噪猾。
for( var i = 1; i <= 5; i++){
? ? ? ? ? ?let j = i;
? ? ? ? ?? setTimeout(function(){
? ? ? ? ? ? console.log(j);
? ? ? ? ? },i*1000);
}
當(dāng)然霉祸,還有一種情形就是for循環(huán)的頭部用let 聲明(如下代碼),這樣它會(huì)被賦予一種特殊行為袱蜡,就是每次循環(huán) 丝蹭,i 都會(huì)被聲明一次,然后都會(huì)用上一次迭代結(jié)束的值賦予給 i坪蚁,所以同樣可以達(dá)到目的奔穿。
for( let i = 1; i <= 5; i++){
? ? ? ? ?? setTimeout(function(){
? ? ? ? ? ? console.log(i);
? ? ? ? ? },i*1000);
}
是不是覺(jué)得很酷、很神奇呢敏晤,塊作用域和閉包聯(lián)手起來(lái)原來(lái)這么厲害啊贱田。