《你不知道的JavaScript》真的是一本好書,閱讀這本書慕匠,我有多次“哦幔妨,原來(lái)是這樣”的感覺斥滤,以前自以為理解了(其實(shí)并非真的理解)的概念,這一次真的理解得更加透徹了丸升。關(guān)于本書铆农,我會(huì)寫好幾篇讀書筆記用以記錄那些讓我恍然大悟的瞬間,本文是第一篇《弄懂JavaScript的作用域和閉包》狡耻。
看正文之前墩剖,先考你幾個(gè)問你,如果你能清晰的回答夷狰,那本文可能對(duì)你作用不大岭皂,如果有一些疑問,那我們就一起來(lái)解開這些疑問吧沼头。
考考你
- 標(biāo)識(shí)符是什么爷绘?
LHS
,RHS
又是什么进倍,其意義何在土至?
- 什么是詞法作用域?javascript語(yǔ)言中那些東西會(huì)影響作用域猾昆?
- 我們一直都在聽說(shuō)的各種提升(函數(shù)提升陶因,變量提升)究竟要怎么理解?
- 在我們平時(shí)的編程中毡庆,那些地方用到了閉包坑赡?(悄悄告訴你烙如,我之前也能把閉包的概念背的滾瓜亂熟,但是卻一直以為自己平時(shí)很少用到閉包毅否,后來(lái)才發(fā)現(xiàn)亚铁,原來(lái)一直都在用啊。螟加。)
正文從這里開始
從瀏覽器如何編譯JS代碼說(shuō)起
很久以來(lái)我就在思考徘溢,當(dāng)我們把代碼交給瀏覽器,瀏覽器是如何把代碼轉(zhuǎn)換為活靈活現(xiàn)的網(wǎng)頁(yè)的捆探。JS引擎在執(zhí)行我們的代碼前然爆,瀏覽器對(duì)我們的代碼還做了什么,這個(gè)過(guò)程對(duì)我來(lái)說(shuō)就像黑匣子一般黍图,神秘而又讓人好奇曾雕。
理解var a = 2
我們每天都會(huì)寫類似var a = 2
這樣的簡(jiǎn)單的JS代碼,可是瀏覽器是機(jī)器助被,它可只認(rèn)識(shí)二進(jìn)制的0和1剖张,var a = 2
對(duì)它來(lái)說(shuō)肯定比外語(yǔ)對(duì)我們還難。不過(guò)有困難不要緊揩环,至少我們現(xiàn)在問題清晰了搔弄,要知道它是如何把有意義的人類字符轉(zhuǎn)化為符合一定規(guī)則的機(jī)器的0 和 1 。
想想我們是如何閱讀一句話的(可以想想我們不那么熟悉的外語(yǔ))丰滑,我們不熟悉英語(yǔ)的時(shí)候顾犹,我們其實(shí)優(yōu)先去理解的是一個(gè)個(gè)的詞,這些詞按照一定的規(guī)則就成了有意義的句子褒墨。瀏覽器其實(shí)也是如此var a = 2
炫刷,瀏覽器其實(shí)看到的是var
,a
,=
,2
這是一個(gè)個(gè)的詞。這個(gè)過(guò)程叫做詞法解析階段貌亭,換句話說(shuō)是這個(gè)過(guò)程會(huì)將由字符組成的字符串分解成(對(duì)編程語(yǔ)言來(lái)說(shuō))有意義的代碼塊柬唯。
就像我們按照語(yǔ)法規(guī)則組合單詞為句子一樣,瀏覽器也會(huì)把上述已經(jīng)分解好的代碼塊組合為代表了程序語(yǔ)法結(jié)構(gòu)的樹(AST)圃庭,這個(gè)階段稱為語(yǔ)法分析階段锄奢,AST對(duì)瀏覽器來(lái)說(shuō)已經(jīng)是有意義的外語(yǔ)了,不過(guò)距離它直接理解還差一步代碼生成剧腻,轉(zhuǎn)換代碼為有意義的機(jī)器語(yǔ)言(二進(jìn)制語(yǔ)言)拘央。
我們總結(jié)一下經(jīng)歷的三階段
- 詞法分析:分解代碼為有意義的詞語(yǔ);
* 語(yǔ)法分析:把有意義的詞語(yǔ)按照語(yǔ)法規(guī)則組合成代表程序語(yǔ)法結(jié)構(gòu)的樹(AST)书在;
* 代碼生成:將 AST 轉(zhuǎn)換為可執(zhí)行代碼
通過(guò)上述三個(gè)階段灰伟,瀏覽器已經(jīng)可以運(yùn)行我們得到的可執(zhí)行代碼了,這三個(gè)階段還有一個(gè)合稱呼叫做編譯階段。我們把之后對(duì)可執(zhí)行代碼的執(zhí)行稱為運(yùn)行階段栏账。
JS的作用域在何時(shí)確定
編程語(yǔ)言中帖族,作用域一般來(lái)說(shuō)有兩種,詞法作用域和動(dòng)態(tài)作用域挡爵。詞法作用域就是依賴編程時(shí)所寫的代碼結(jié)構(gòu)確定的作用域竖般,一般來(lái)說(shuō)在編譯結(jié)束后,作用域就已經(jīng)確定茶鹃,代碼運(yùn)行過(guò)程中不再改變涣雕。而動(dòng)態(tài)作用域聽名字就知道是在代碼運(yùn)行過(guò)程中作用域會(huì)動(dòng)態(tài)改變。一般認(rèn)為我們的javascript的作用域是詞法作用域(說(shuō)一般闭翩,是因?yàn)閖avascript提供了一些動(dòng)態(tài)改變作用域的方法挣郭,后文會(huì)有介紹)。
詞法作用域就是依賴編程時(shí)所寫的代碼結(jié)構(gòu)確定的作用域疗韵,對(duì)比一下瀏覽器在編譯階段做的事情兑障,我們發(fā)現(xiàn),詞法作用域就是在編譯階段確定的伶棒⊥荩看到這里是不是突然理解了為什么以前我們常常聽到的“函數(shù)的作用域在函數(shù)定義階段就確定了”這句話了彩库。接下來(lái)我們就來(lái)說(shuō)明函數(shù)作用域是按照什么規(guī)則確定的肤无。
JS中的作用域
作用域是什么?
關(guān)于作用域是什么骇钦?《You don’t know js》給出了這么一個(gè)概念:
使用一套嚴(yán)格的規(guī)則來(lái)分辨哪些標(biāo)識(shí)符對(duì)那些語(yǔ)法有訪問權(quán)限宛渐。
好吧,好抽象的一句話眯搭,標(biāo)識(shí)符又是什么呢窥翩?作用域到底要怎么理解啊鳞仙?我們一個(gè)個(gè)來(lái)看寇蚊。
標(biāo)識(shí)符:
我們知道,當(dāng)我們的程序運(yùn)行的時(shí)候棍好,我們的數(shù)據(jù)(”字符串”仗岸,“對(duì)象”,“函數(shù)”等等都是要載入內(nèi)存的)借笙。那我們?cè)撊绾卧L問到對(duì)應(yīng)的內(nèi)存區(qū)域呢扒怖,標(biāo)識(shí)符就在這時(shí)候起作用了,通過(guò)它我們就能找到對(duì)應(yīng)的數(shù)據(jù)业稼,從這個(gè)角度來(lái)看盗痒,變量名,函數(shù)名等等都是標(biāo)識(shí)符低散。
對(duì)標(biāo)識(shí)符的操作
知道了標(biāo)識(shí)符俯邓,我們來(lái)想想骡楼,平時(shí)我們會(huì)對(duì)標(biāo)識(shí)符進(jìn)行哪些操作。其實(shí)無(wú)外乎兩種稽鞭,看下面的代碼:
// 第一種定義了標(biāo)識(shí)符`a`并把數(shù)值2賦值給了`a`這種操作有一個(gè)專門的術(shù)語(yǔ)叫做`LHS`
var a = 2;
// 第二種君编,var b = a ,其實(shí)對(duì)應(yīng)a ,b 兩個(gè)操作符是不同的操作,對(duì)b來(lái)說(shuō)是一個(gè)賦值操作川慌,這是LHS,但是對(duì)a來(lái)說(shuō)卻是取到a對(duì)應(yīng)的值吃嘿,這種操作也有一個(gè)專門的術(shù)語(yǔ)叫做“RHS”
var b = a;
小結(jié)一下,對(duì)標(biāo)識(shí)符來(lái)說(shuō)有以下兩種操作
- 賦值操作(LHS)梦重;常見的是函數(shù)定義兑燥,函數(shù)傳參,變量賦值等等
* 取值操作(RHS)琴拧;常見包括函數(shù)調(diào)用降瞳,
再回過(guò)頭來(lái)看作用域
明白了標(biāo)識(shí)符及對(duì)標(biāo)識(shí)符的兩種操作,我們可以很容易的理解作用域了蚓胸,作用域其實(shí)就是定義了我們的呈現(xiàn)在運(yùn)行期挣饥,進(jìn)行標(biāo)識(shí)符操作的范圍,對(duì)應(yīng)到實(shí)際問題來(lái)說(shuō)沛膳,就是我們熟悉的函數(shù)或者變量可以在什么地方調(diào)用扔枫。
作用域也可以看做是一套依據(jù)名稱查找變量的規(guī)則。那我們?cè)偌?xì)看一下這個(gè)規(guī)則锹安,在當(dāng)前作用域中無(wú)法找到某個(gè)變量時(shí)短荐,引擎就會(huì)在外層嵌套的作用域中繼續(xù)查找,直到找到該變量叹哭, 或抵達(dá)最外層的作用域(也就是全局作用域)為止忍宋。
這里提到了嵌套一詞,我們接下來(lái)看js中那些因素可以形成作用域风罩。
JS中的作用域類型
函數(shù)作用域
函數(shù)作用域是js中最常見的作用域了糠排,函數(shù)作用域給我們最直觀的體會(huì)就是,內(nèi)部函數(shù)可以調(diào)用外部函數(shù)中的變量超升。一層層的函數(shù)入宦,很直觀的就形成了嵌套的作用域。不過(guò)只說(shuō)這一點(diǎn)真對(duì)不起本文的標(biāo)題廓俭,還記得我們常常聽到的“如果在函數(shù)內(nèi)部我們給一個(gè)未定義的變量賦值云石,這個(gè)變量會(huì)轉(zhuǎn)變?yōu)橐粋€(gè)全局變量”。對(duì)我來(lái)說(shuō)之前這句話幾乎是背下來(lái)的研乒,我一直都沒能理解汹忠。我們從對(duì)標(biāo)識(shí)符的操作的角度來(lái)理解這句話。
var a = 1;
function foo(){
// b第一次出現(xiàn)在函數(shù)foo中
b = a ;
}
foo();
// 全局可以訪問到b
console.log(b); //1
在我們調(diào)用foo()
時(shí),對(duì)b其實(shí)是進(jìn)行了LHS操作(取得a的值并賦值給b)宽菜,b前面并不存在var let 等谣膳,因此瀏覽器首先在foo()
作用域里面查找b這個(gè)標(biāo)識(shí)符,結(jié)果在b里面沒有找到铅乡,安裝作用域的規(guī)則继谚,瀏覽器會(huì)繼續(xù)在foo()
的外層作用域?qū)ふ覙?biāo)識(shí)符b,結(jié)果還是沒有找到阵幸,說(shuō)明在這次查詢標(biāo)識(shí)符b的范圍內(nèi)并不存在已經(jīng)定義的b花履,在非嚴(yán)格模式下LHS操作會(huì)在可查找范圍的最外層(也就是全局)定義一個(gè)b,因此b也就成了一個(gè)全局的變量了(嚴(yán)格模式LHS找不到返回ReferenceError錯(cuò)誤)挚赊。這樣那句話就可以理解了诡壁。同樣值得我們注意的是對(duì)操作符進(jìn)行RHS操作會(huì)出現(xiàn)不同的情況,無(wú)論嚴(yán)格或者非嚴(yán)格模式RHS找不到對(duì)返回ReferenceError錯(cuò)誤(對(duì)RHS找到的值進(jìn)行不合理的操作會(huì)返回錯(cuò)誤TypeError
(作用域判別成功荠割,操作非法妹卿。))。
閉包:閉包是基于詞法作用域書寫代碼時(shí)所產(chǎn)生的自然結(jié)果蔑鹦,你甚至不需要為了利用它們而有意 識(shí)地創(chuàng)建閉包夺克。閉包的創(chuàng)建和使用在你的代碼中隨處可見。你缺少的是根據(jù)你自己的意愿 來(lái)識(shí)別嚎朽、擁抱和影響閉包的思維環(huán)境铺纽。
塊作用域
除了函數(shù)作用域,JS也提供塊作用域火鼻。我們應(yīng)該明確室囊,作用域是針對(duì)標(biāo)識(shí)符來(lái)說(shuō)的,塊作用域把標(biāo)識(shí)符限制在{}
中魁索。
ES6 提供的let
,const
方法聲明的標(biāo)識(shí)符都會(huì)固定于塊中。常被大家忽略的try/catch
的catch
語(yǔ)句也會(huì)創(chuàng)建一個(gè)塊作用域盼铁。
改變函數(shù)作用域的方法
一般說(shuō)來(lái)詞法作用域在代碼編譯階段就已經(jīng)確定粗蔚,這種確定性其實(shí)是很有好處的,代碼在執(zhí)行過(guò)程中饶火,能夠預(yù)測(cè)在執(zhí)行過(guò)程中如何對(duì)它們進(jìn)行查找鹏控。能夠提高代碼運(yùn)行階段的執(zhí)行效率。不過(guò)JS也提供動(dòng)態(tài)改變作用域的方法肤寝。eval()
函數(shù)和with
關(guān)鍵字.
eval()
方法:
這個(gè)方法接受一個(gè)字符串為參數(shù)当辐,并將其中的內(nèi)容視為好像在書寫時(shí)就存在于程序中這個(gè)位置的代碼。換句話說(shuō)鲤看,可以在你寫的代碼中用程序生成代碼并運(yùn)行缘揪,就好像代碼是寫在那個(gè)位置的一樣。
function foo(str,a){
eval(str);//欺騙作用域,詞法階段階段foo()函數(shù)中并沒有定義標(biāo)識(shí)符,但是在函數(shù)運(yùn)行階段卻臨時(shí)定義了一個(gè)b找筝;
console.log(a,b);
}
var b = 2;
foo("var b =3;",1);//1,3
// 嚴(yán)格模式下蹈垢,`eval()`會(huì)產(chǎn)生自己的作用域,無(wú)法修改所在的作用域
function foo(str){
'use strict';
eval(str);
console.log(a);//ReferenceError: a is not de ned
}
foo('var a =2');
eval()
有時(shí)候挺有用袖裕,但是性能消耗很大曹抬,可能也會(huì)帶來(lái)安全隱患,因此不推薦使用急鳄。
with
關(guān)鍵字:
with 通常被當(dāng)作重復(fù)引用同一個(gè)對(duì)象中的多個(gè)屬性的快捷方式谤民。
var obj = {
a: 1,
b: 2,
c: 3
};
// 單調(diào)乏味的重復(fù) "obj" obj.a = 2;
obj.b = 3;
obj.c = 4;
// 簡(jiǎn)單的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a被泄漏到全局作用域上了!
// 執(zhí)行了LHS查詢疾宏,不存在就在全局創(chuàng)建了一個(gè)赖临。
// with 聲明實(shí)際上是根據(jù)你傳遞給它的對(duì)象憑空創(chuàng)建了一個(gè)全新的詞法作用域。
with
也會(huì)帶來(lái)性能的損耗灾锯。
JavaScript 引擎會(huì)在編譯階段進(jìn)行數(shù)項(xiàng)的性能優(yōu)化兢榨。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置顺饮,才能在執(zhí)行過(guò)程中快速找到標(biāo)識(shí)符吵聪。
聲明提升
作用域關(guān)系到的是標(biāo)識(shí)符的作用范圍,而標(biāo)識(shí)符的作用范圍和它的聲明位置是密切相關(guān)的兼雄。在js
中有一些關(guān)鍵字是專門用來(lái)聲明標(biāo)識(shí)符的(比如var
,let
,const
)吟逝,非匿名函數(shù)的定義也會(huì)聲明標(biāo)識(shí)符。
關(guān)于聲明也許大家都聽說(shuō)過(guò)聲明提升一詞赦肋。我們來(lái)分析一下造成聲明提升的原因块攒。
我們已經(jīng)知道引擎會(huì)在解釋 JavaScript 代碼之前首先對(duì)其進(jìn)行編譯。編譯階段中的一部分工作就是找到所有的聲明佃乘,并用合適的作用域?qū)⑺鼈冴P(guān)聯(lián)起來(lái)(詞法作用域的核心)囱井。
這樣的話,聲明好像被提到了前面趣避。
值得注意的是每個(gè)作用域都會(huì)進(jìn)行提升操作庞呕。聲明會(huì)被提升到所在作用域的頂部。
不過(guò)并非所有的聲明都會(huì)被提升程帕,不同聲明提升的權(quán)重也不同住练,具體來(lái)說(shuō)函數(shù)聲明會(huì)被提升,函數(shù)表達(dá)式不會(huì)被提升(就算是有名稱的函數(shù)表達(dá)式也不會(huì)提升)愁拭。
通過(guò)var
定義的變量會(huì)提升讲逛,而let
和const
進(jìn)行的聲明不會(huì)提升。
函數(shù)聲明和變量聲明都會(huì)被提升岭埠。但是一個(gè)值得注意的細(xì)節(jié)也就是函數(shù)會(huì)首先被提升盏混,然后才是變量蔚鸥,也就是說(shuō)如果一個(gè)變量聲明和一個(gè)函數(shù)聲明同名,那么就算在語(yǔ)句順序上變量聲明在前括饶,該標(biāo)識(shí)符還是會(huì)指向相關(guān)函數(shù)株茶。
如果變量或函數(shù)有重復(fù)聲明以會(huì)第一次聲明為主。
最后一點(diǎn)需要注意的是:
聲明本身會(huì)被提升图焰,而包括函數(shù)表達(dá)式的賦值在內(nèi)的賦值操作并不會(huì)提升启盛。
作用域的一些應(yīng)用
看到這里,我想大家對(duì)JS的作用域應(yīng)該有了一個(gè)比較細(xì)致的了解技羔。下面說(shuō)一下對(duì)JS作用域的一些拓展應(yīng)用僵闯。
最小特權(quán)原則
也叫最小授權(quán)或最小暴露原則。這個(gè)原則是指在軟件設(shè)計(jì)中藤滥,應(yīng)該最小限度地暴露必要內(nèi)容鳖粟,而將其他內(nèi)容都“隱藏”起來(lái),比如某個(gè)模塊或?qū)ο蟮?API 設(shè)計(jì)拙绊。也就是盡可能多的把部分代碼私有化向图。
函數(shù)可以產(chǎn)生自己的作用域,因此我們可以采用函數(shù)封裝(函數(shù)表達(dá)式和函數(shù)聲明都可以)的方法來(lái)實(shí)現(xiàn)這一原則标沪。
// 函數(shù)表達(dá)式
var a = 2;
(function foo() { // <-- 添加這一行 var a = 3;
console.log(a); // 3
})(); // <-- 以及這一行
console.log( a ); // 2
這里順便說(shuō)明一下如何區(qū)分函數(shù)表達(dá)式和函數(shù)聲明:
如果 function 是聲明中 的第一個(gè)詞榄攀,那么就是一個(gè)函數(shù)聲明,否則就是一個(gè)函數(shù)表達(dá)式金句。
函數(shù)聲明和函數(shù)表達(dá)式之間最重要的區(qū)別是它們的名稱標(biāo)識(shí)符將會(huì)綁定在何處檩赢。函數(shù)表達(dá)式可以是匿名的,而函數(shù)聲明則不可以省略函數(shù)名——在 JavaScript 的語(yǔ)法中這是非法的违寞。
可以使用立即執(zhí)行的函數(shù)表達(dá)式(IIFE)的方式來(lái)封裝贞瞒。
立即執(zhí)行的函數(shù)表達(dá)式(IIFE)
var a = 2;
(function foo() {
var a = 3;
console.log(a); // 3
})();
console.log(a); // 2
函數(shù)表達(dá)式后面加上一個(gè)括號(hào)后會(huì)立即執(zhí)行。
(function(){ .. }())
是IIFE的另外一種表達(dá)方式括號(hào)加在里面和外面趁曼,功能是一樣的军浆。
順便說(shuō)一下,IIFE 的另一個(gè)非常普遍的進(jìn)階用法是把它們當(dāng)作函數(shù)調(diào)用并傳遞參數(shù)進(jìn)去彰阴。
var a = 2;
(function IIFE(global) {
var a = 3;
console.log(a); // 3 console.log( global.a ); // 2
})(window);
console.log(a); // 2
閉包
一般大家都會(huì)這么形容閉包瘾敢。
當(dāng)一個(gè)函數(shù)的返回值是另外一個(gè)函數(shù),而返回的那個(gè)函數(shù)如果調(diào)用了其父函數(shù)內(nèi)部的其它變量尿这,如果返回的這個(gè)函數(shù)在外部被執(zhí)行,就產(chǎn)生了閉包庆杜。
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 —— 這就是閉包的效果射众。在函數(shù)外訪問了函數(shù)內(nèi)的標(biāo)識(shí)符
// bar()函數(shù)持有對(duì)其父作用域的引用,而使得父作用域沒有被銷毀晃财,這就是閉包
一般來(lái)說(shuō)叨橱,由于垃圾回收機(jī)制的存在典蜕,函數(shù)在執(zhí)行完以后會(huì)被銷毀,不再使用的內(nèi)存空間罗洗。上例中由于看上去 foo()
的內(nèi)容不會(huì)再被使用愉舔,所以很自然地會(huì)考慮對(duì)其進(jìn)行回收。而閉包的“神奇”之處正是可以阻止這件事情的發(fā)生(以前總有人說(shuō)要減少使用閉包伙菜,害怕內(nèi)存泄漏什么的轩缤,其實(shí)這個(gè)也不大比擔(dān)心)。
其實(shí)上面這個(gè)定義贩绕,在好久之前我就知道火的,不過(guò)同時(shí)我也誤以為我平時(shí)很少用到閉包,因?yàn)槲艺娴牟]有主動(dòng)去用過(guò)閉包淑倾,不過(guò)其實(shí)我錯(cuò)了馏鹤,無(wú)意中,我一直在使用閉包娇哆。
本質(zhì)上無(wú)論何時(shí)何地湃累,如果將函數(shù)(訪問它們各自的詞法作用域)當(dāng)作第一 級(jí)的值類型并到處傳遞,你就會(huì)看到閉包在這些函數(shù)中的應(yīng)用碍讨。在定時(shí)器治力、事件監(jiān)聽器、 Ajax請(qǐng)求垄开、跨窗口通信琴许、Web Workers或者任何其他的異步(或者同步)任務(wù)中,只要使 用了回調(diào)函數(shù)溉躲,實(shí)際上就是在使用閉包!
所以你應(yīng)該知道榜田,你已經(jīng)用過(guò)很多次閉包了。
這里說(shuō)一個(gè)大家可能都遇到過(guò)的坑锻梳,一個(gè)沒有正確理解作用域和閉包造成的坑箭券。
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
// 其實(shí)我們想得到的結(jié)果是1,2疑枯,3辩块,4,5荆永,結(jié)果卻是五個(gè)6
我們分析一下造成這個(gè)結(jié)果的原因:
我們?cè)噲D假設(shè)循環(huán)中的每個(gè)迭代在運(yùn)行時(shí)都會(huì)給自己“捕獲”一個(gè) i 的副本废亭。但是根據(jù)作用域的工作原理,實(shí)際情況是盡管循環(huán)中的五個(gè)函數(shù)是在各個(gè)迭代中分別定義的(前面說(shuō)過(guò)以第一次定義為主具钥,后面的會(huì)被忽略)豆村, 但是它們都被封閉在一個(gè)共享的全局作用域中,因?yàn)樵跁r(shí)間到了執(zhí)行timer函數(shù)時(shí),全局里面的這個(gè)i就是6骂删,因此無(wú)法達(dá)到預(yù)期掌动。
理解了是作用域的問題四啰,這里我們有兩種解決辦法:
// 辦法1
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
//通過(guò)一個(gè)立即執(zhí)行函數(shù),為每次循環(huán)創(chuàng)建一個(gè)單獨(dú)的作用域粗恢。
}
// 辦法2
for (var i = 1; i <= 5; i++) {
let j = i; // 是的柑晒,閉包的塊作用域!
setTimeout( function timer() {
console.log(j);
}, j * 1000);
}
// let 每次循環(huán)都會(huì)創(chuàng)建一個(gè)塊作用域
現(xiàn)在的開發(fā)都離不開模塊化,下面說(shuō)說(shuō)模塊是如何利用閉包的眷射。
模塊是如何利用閉包的:
最常見的實(shí)現(xiàn)模塊模式的方法通常被稱為模塊暴露匙赞。
我們來(lái)看看如何定義一個(gè)模塊
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
// 返回的是一個(gè)對(duì)象,對(duì)象中可能包含各種函數(shù)
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
// 在外面調(diào)用返回對(duì)象中的方法就形成了閉包
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
模塊的兩個(gè)必要條件:
- 必須有外部的封閉函數(shù)凭迹,該函數(shù)必須至少被調(diào)用一次
- 封閉函數(shù)必須返回至少一個(gè)內(nèi)部函數(shù)罚屋,這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態(tài)嗅绸。
文章寫到這里也差不多該結(jié)束了脾猛,謝謝你的閱讀,希望你有所收獲鱼鸠。