JavaScript?中的作用域與閉包
前言:
前幾天面試中,面試官拋出一道題,問我輸出結(jié)果是啥:
var arr = []
for (var i = 0; i < 3; i++) {
arr[i] = function() {
console.log(i);
}
}
arr[0]()
arr[1]()
arr[2]()無知的我脫口而出:“三個(gè)2”,面試官眉頭一皺:“你再仔細(xì)看看”。哦豁,大事不妙,趕緊仔細(xì)看看,不對是三個(gè)3。面試官點(diǎn)了點(diǎn)頭,心想應(yīng)該是答對了。接著面試官又問我,怎么修改呢。心想這不就是閉包嗎,var改let嗎。接著面試官又問我其他方法呢,我說用立即執(zhí)行函數(shù)。結(jié)果到了手寫的時(shí)候突然懵了,背的八股文忘了,然后就尷尬了。。。
看來只背背八股文還是不行,所以今天針對這個(gè)問題,仔細(xì)學(xué)習(xí)了一下前因后果。
一、JavaScript 是一門編譯語言
通常 JavaScript 被歸類于“解釋性語言”或“腳本語言”等,作為開發(fā)Web 頁面的腳本語言而出名。但是事實(shí)上,它是一門編譯語言。
MDN對JavaScript的定義如下:
JavaScript (JS) 是一種具有函數(shù)優(yōu)先的輕量級,解釋型或即時(shí)編譯型的編程語言。
JavaScript 是一種基于原型編程、多范式的動態(tài)腳本語言,并且支持面向?qū)ο?、命令式和聲明式(如函?shù)式編程)風(fēng)格。
—— MDN
1.1 傳統(tǒng)編譯語言的編譯步驟
(1)分詞/詞法分析(Tokenizing/Lexing)將由字符組成的字符串分解成(對編程語言來說)有意義的代碼塊,這些代碼塊被稱為詞法單元(token)。
(2)解析/語法分析(Prsing)將詞法單元流(數(shù)組)轉(zhuǎn)換成一個(gè)由元素逐級嵌套所組成的代表了程序語法結(jié)構(gòu)的樹。這個(gè)樹被稱為“抽象語法樹”(Abstract Syntax Tree,AST)。
(3)代碼生成將 AST 轉(zhuǎn)換為可執(zhí)行代碼。這個(gè)過程與語言、目標(biāo)平臺等息息相關(guān);例如window下C語言編譯最終得到.exe文件。
1.2 JavaScript 與傳統(tǒng)編譯語言的區(qū)別
(1)JavaScript 與傳統(tǒng)編譯語言不同,它不是提前編譯的,編譯結(jié)果也不能在分布式系統(tǒng)中移植。 (2)JavaScript引擎負(fù)責(zé)整個(gè)JavaScript程序的編譯及執(zhí)行過程,編譯器負(fù)責(zé)語法分析及代碼生成等,相對于傳統(tǒng)編譯語言的編譯器更加復(fù)雜(例如:在語法分析和代碼生成階段有特定的步驟來對運(yùn)行性能進(jìn)行優(yōu)化)
(3)大部分情況下JavaScript 編譯發(fā)生在代碼執(zhí)行前的幾微秒(甚至更短)時(shí)間內(nèi)。
二、作用域(Scope)
作用域:負(fù)責(zé)收集并維護(hù)由所有標(biāo)識符(變量)組成的一系列查詢,并實(shí)施一套非常嚴(yán)格的規(guī)則,確定當(dāng)前執(zhí)行的代碼對這些標(biāo)識符的訪問權(quán)限。—— 《你不知道的JavaScript 上卷》
在了解什么是作用域前,首先來看看var a = 2;是如何進(jìn)行處理的。可能大部分和我一樣認(rèn)為這就是一句聲明,但是JavaScript認(rèn)為這里面又兩個(gè)完全不同的聲明,一個(gè)由編譯器在編譯時(shí)處理,另一個(gè)由引擎在運(yùn)行時(shí)處理。
- 首先編譯器會將這段程序分解成詞法單元,然后將詞法單元解析成一個(gè)樹結(jié)構(gòu)
- 緊接進(jìn)行代碼生成,編譯器會進(jìn)行如下處理:
- 遇到 var a,編譯器會詢問作用域是否已經(jīng)有一個(gè)該名稱的變量存在于同一個(gè)作用域的集合中。如果是,編譯器會忽略該聲明,繼續(xù)進(jìn)行編譯;否則它會要求作用域在當(dāng)前作用域的集合中聲明一個(gè)新的變量,并命名為 a。
- 接下來編譯器會為引擎生成運(yùn)行時(shí)所需的代碼,這些代碼被用來處理 a = 2 這個(gè)賦值操作。引擎運(yùn)行時(shí)會首先詢問作用域,在當(dāng)前的作用域集合中是否存在一個(gè)叫作 a 的變量。如果是,引擎就會使用這個(gè)變量;如果否,引擎會繼續(xù)查找該變量(即在作用域鏈上查找)如果引擎最終找到了 a 變量,就會將 2 賦值給它。否則引擎就會舉手示意并拋出一個(gè)異常!
2.1 LHS查詢 和 RHS查詢
如上例子,編譯器為引擎生成了為引擎生成了運(yùn)行時(shí)所需的代碼后,引擎執(zhí)行它時(shí),是如何查找變量a的呢?這里就要引入LHS查詢和RHS查詢兩個(gè)術(shù)語了。
(1)LHS 查詢:試圖找到變量的容器本身,從而可以對其賦值
(2)RHS 查詢:查找某個(gè)變量的值
以下面程序?yàn)槔?,對LHS 和 RHS 做更深一步的解釋:
function foo(a) {
console.log( a ); // 2
}
foo( 2 );- 首先:foo() 函數(shù)的調(diào)用,需要對foo進(jìn)行RHS查詢,即查找 foo 的值
- 緊接著執(zhí)行foo(2)時(shí),這里傳遞參數(shù)時(shí),隱式進(jìn)行了 a = 2,那么這里需要對 a 進(jìn)行LHS查詢,找到a后再將2賦值給a。
- 進(jìn)入foo函數(shù)內(nèi)部,然后對console進(jìn)行RHS查詢,然后對a進(jìn)行RHS查詢,傳遞進(jìn)log()。
再看個(gè)例子:
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );其中有:
3次 LHS 查詢
- c = ..
- a = 2
- b = ..
4次 RHS 查詢
- foo(..)
- = a
- a ..
- .. b
2.2 作用域嵌套
作用域簡單來說就是根據(jù)名稱查找變量的一套規(guī)則,但實(shí)際情況中,上述的查詢的可能不僅限于一個(gè)作用域。
當(dāng)一個(gè)塊或函數(shù)嵌套在另一個(gè)塊或函數(shù)中時(shí),就發(fā)生了作用域的嵌套。
因此,在當(dāng)前作用域中無法找到某個(gè)變量時(shí),引擎就會在外層嵌套的作用域中繼續(xù)查找,直到找到該變量,或抵達(dá)最外層的作用域(即:全局作用域)為止。
function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4例如上述代碼中,對b進(jìn)行RHS查詢時(shí),無法在當(dāng)前函數(shù)foo的作用域中完成,需要向上一級作用域查找,即在全局作用域中完成了。
LHS查詢和RHS查詢都會在當(dāng)前執(zhí)行的作用域開始查找,如果沒有找到,則會向上一級查找,直到查找成功或者達(dá)到全局作用域。達(dá)到全局作用域,無論是否找到,都會停止查詢過程。
2.3 ReferenceError 和 TypeError
若在任何作用域中都無法查找到變量,那么引擎就會拋出異常。但是針對LHS查詢失敗和RHS查詢失敗拋出的異常是不同的。
(1)ReferenceError
console.log(a);
上述代碼在執(zhí)行時(shí),會拋出 ReferenceError 。這是因?yàn)樵趯?a 進(jìn)行RHS查詢時(shí),是無法查找到改變量的。這是因?yàn)樽兞?a ”未聲明“,不存在于任何作用域中。
所以,RHS 查詢在所有嵌套的作用域中遍尋不到所需的變量,引擎就會拋出 ReferenceError異常。
相比之下,LHS查詢在所有嵌套的作用域中查詢不到目標(biāo)變量時(shí),全局作用域會創(chuàng)建一個(gè)具有該名稱的變量,并將其返還給引擎。但是,如果是在”嚴(yán)格模式“下,引擎也會拋出 ReferenceError.
// 嚴(yán)格模式下 "use strict" a = 2; // ReferenceError // 非嚴(yán)格模式下 a = 2 // 執(zhí)行成功
即:
- RHS查詢失敗時(shí):引擎會拋出 ReferenceError
- LHS查詢失敗時(shí):
- 嚴(yán)格模式: 引擎會拋出 ReferenceError;
- 非嚴(yán)格模式:全局作用域會創(chuàng)建一個(gè)具有該名稱的變量,并將其返還給引擎
(2)TypeError
如果 RHS 查詢找到了一個(gè)變量,但是你嘗試對這個(gè)變量的值進(jìn)行不合理的操作,比如試圖對一個(gè)非函數(shù)類型的值進(jìn)行函數(shù)調(diào)用,或著引用 null 或 undefined 類型的值中的屬性,那么引擎會拋出另外一種類型的異常,叫作 TypeError。
// 對非函數(shù)類型的值進(jìn)行調(diào)用 let a = 0; a(); // 引用undefined類型的值的屬性 let b; b.name;
(3)ReferenceError 和 TypeError 的區(qū)別
ReferenceError 表示RHS查詢失敗,或嚴(yán)格模式下的LHS查詢失敗
TypeError 則代表RHS查詢成功了,但是對結(jié)果的操作是非法或不合理的。
小結(jié)
作用域是一套規(guī)則,用于確定在何處以及如何查找變量(標(biāo)識符)。如果查找的目的是對變量進(jìn)行賦值,那么就會使用 LHS 查詢;如果目的是獲取變量的值,就會使用 RHS 查詢。
LHS 和 RHS 查詢都會在當(dāng)前執(zhí)行作用域中開始,如果沒找到,就會向上級作用域繼續(xù)查找目標(biāo)標(biāo)識符,這樣每次上升一級作用域,最后抵達(dá)全局作用域(頂層),無論找到或沒找到都將停止。
不成功的 RHS 引用會導(dǎo)致拋出 ReferenceError 異常。不成功的 LHS 引用會導(dǎo)致自動隱式地創(chuàng)建一個(gè)全局變量(非嚴(yán)格模式下),該變量使用 LHS 引用的目標(biāo)作為標(biāo)識符,或者拋出 ReferenceError 異常(嚴(yán)格模式下)。
三、詞法作用域
第二節(jié)中提到作用域可以定義為一套規(guī)則,但是這套規(guī)則又是如何去定義的呢?
作用域主要有兩種主要的工作模型:詞法作用域 和 動態(tài)作用域,其中 JavaScript 采用的是詞法作用域
3.1 詞法階段
如第一節(jié)中介紹的,大部分標(biāo)準(zhǔn)語言編譯器的第一個(gè)工作階段叫作詞法分析。即對源代碼中的字符進(jìn)行檢查,識別出每個(gè)單詞。
簡單來說,詞法作用域就是定義在詞法階段的作用域。即由代碼中變量的書寫位置來決定的。

3.2 詞法作用域 查找規(guī)則
(1)作用域查找是找從運(yùn)行時(shí)所處的最內(nèi)部作用域開始,逐級向外,直到遇見第一個(gè)匹配的標(biāo)識符為止。
(2)遮蔽效應(yīng):在多層嵌套的作用域中可以定義同名的標(biāo)識符,但是內(nèi)部的標(biāo)識符會”遮蔽“外部的標(biāo)識符。
(3)全局變量會自動成為全局對象的屬性。所以可以通過全局對象的引用來間接訪問全局變量。
// a是全局變量 var a = 1; // 瀏覽器中全局對象一般為window console.log(window.a) // 1
所以,當(dāng)全局變量在內(nèi)部作用域被同名變量“遮蔽”時(shí),可通過該方法訪問到全局變量,例如:
// a是全局變量
var a = 1;
funcion foo() {
let a = 2;
console.log(a); // 2
console.log(window.a); // 1
}但是,對于非全局變量來說,如果被遮蔽了,就無法訪問到。
(4)無論函數(shù)何時(shí)、何處以及如何被調(diào)用,它的詞法作用域都只由被聲明時(shí)所處的位置決定。(即與代碼中書寫的位置保持一致)
(5)詞法作用域的查找只會查找\color{red}{一級標(biāo)識符}一級標(biāo)識符。
例如:針對foo.a.b,詞法作用域只會試圖查找 foo 標(biāo)識符,找到 foo 這個(gè)變量后,對象屬性訪問規(guī)則會接管對 a 和 b 屬性的訪問。
這也解釋了前面當(dāng)引擎遇到console.log();時(shí),只會對 console 進(jìn)行一次RHS查詢,不會接著對 log 進(jìn)行RHS查詢。
3.3 欺騙詞法 —— eval、with
3.2中說到,詞法作用域是由書寫代碼期間函數(shù)所聲明的位置來定義。但是JavaScript中有兩個(gè)機(jī)制會在運(yùn)行時(shí)“修改”詞法作用域——eval、with。但是很多地方都建議不使用這兩種機(jī)制,因?yàn)槠垓_詞法作用域會導(dǎo)致性能下降。
(1)eval
eval()是全局對象的一個(gè)函數(shù)屬性。eval()的參數(shù)是一個(gè)字符串。如果字符串表示的是表達(dá)式,eval()會對表達(dá)式進(jìn)行求值。如果參數(shù)表示一個(gè)或多個(gè) JavaScript 語句,那么eval()就會執(zhí)行這些語句。 —— MDN
換個(gè)說法,eval可以在書寫的代碼中用程序生成代碼并運(yùn)行,就好像代碼是寫在那個(gè)位置一樣。
在執(zhí)行 eval(..) 之后的代碼時(shí),引擎并不“知道”或“在意”前面的代碼是以動態(tài)形式插入進(jìn)來,并對詞法作用域的環(huán)境進(jìn)行修改的。引擎只會如往常地進(jìn)行詞法作用域查找。
function foo(str, a) {
eval( str ); // 欺騙!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
foo( "", 1 ); // 1, 2從上述代碼可以看出,書寫的代碼中 foo 函數(shù)的詞法作用域并沒有聲明變量 b。但是eval(..) 調(diào)用中的 var b = 3; 這段代碼會被當(dāng)作本來就在那里一樣來處理。因此對foo函數(shù)的詞法作用域進(jìn)行了修改,在foo函數(shù)內(nèi)部創(chuàng)建了一個(gè)變量b,遮蔽了全局變量b,所以輸出 1, 3。
eval(..) 可以在運(yùn)行期修改書寫期的詞法作用域。但個(gè)人覺得其實(shí)并沒有破壞詞法作用域的查找規(guī)則,即把 eval() 的參數(shù)在eval書寫的位置替換eval()。然后再按詞法作用域規(guī)則去查找。
\color{red}{注意:}注意:在嚴(yán)格模式下,eval()在運(yùn)行時(shí)有自己的詞法作用域,所以其中的聲明無法修改所在的作用域。
function foo(str, a) {
"use strict"
eval( str ); // 欺騙!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;console.log(b)", 1 ); //3 1, 2從上述代碼可以看出,嚴(yán)格模式下,在eval()函數(shù)內(nèi)部輸出b,值為3,但是在eval()函數(shù)外部輸出b,值為2.
不推薦使用與 eval() 以及與 eval() 類似的函數(shù)setTimeout(..) 和setInterval(..) 的第一個(gè)參數(shù)可以是字符串,字符串的內(nèi)容可以被解釋為一段動態(tài)生成的函數(shù)代碼。這些功能已經(jīng)過時(shí)且并不被提倡。(目前一般是傳遞回調(diào)函數(shù))
new Function(..) 最后一個(gè)參數(shù)可以接受代碼字符串,并將其轉(zhuǎn)化為動態(tài)生成的函數(shù)(前面的參數(shù)是這個(gè)新生成的函數(shù)的形參)。這種構(gòu)建函數(shù)的語法比eval(..) 略微安全一些,但也要盡量避免使用。
(2)with
'with'語句將某個(gè)對象添加到作用域鏈的頂部,如果在statement中有某個(gè)未使用命名空間的變量,跟作用域鏈中的某個(gè)屬性同名,則這個(gè)變量將指向這個(gè)屬性值。如果沒有同名的屬性,則將拋出ReferenceError異常。—— MDN
with (expression) {
statement
}換種說法,with 可以將一個(gè)沒有或有多個(gè)屬性的對象處理為一個(gè)完全隔離的全新的詞法作用域,因此這個(gè)對象的屬性也會被處理為定義在這個(gè)作用域中的詞法標(biāo)識符。
var c = 3;
let obj = {
a: 1,
b: 2
}
with(obj) {
console.log(a); // 1
var b = 5;
console.log(b); // 5
console.log(c); // 3
console.log(d); // ReferenceError
}對于上述代碼,我們可以這樣理解,with 語句創(chuàng)建了一個(gè)全新的詞法作用域,并把 obj 放在該詞法作用域的頂層(若把該詞法作用域類比為全局作用域,那么obj就是一個(gè)全局對象)。在該全新的作用域中,obj的所有屬性都可以直接訪問。console.log(a)輸出1:當(dāng)前詞法作用域未聲明變量a,所以向上一級查找,obj中包含屬性a,所以輸出1console.log(b)輸出5:當(dāng)前詞法作用域中聲明了變量b,該變量b”遮蔽“了obj中的屬性b,所以輸出5console.log(c)輸出3:當(dāng)前詞法作用域未定義變量c,obj中也沒有屬性c,則繼續(xù)向全局作用域查找,所以輸出3console.log(d)拋出異常ReferenceError:因?yàn)樵诋?dāng)前詞法作用域以及其嵌套的所有詞法作用域中都未聲明變量d,RHS查詢失敗,所以拋出ReferenceError
\color{red}{注意:}注意:在 ECMAScript 5嚴(yán)格模式下,with標(biāo)簽已經(jīng)被禁用。
(3)為什么不推薦使用 eval() 和 with
1. eval() 和 with 對性能的影響JavaScript 引擎會在編譯階段進(jìn)行數(shù)項(xiàng)的性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置,才能在執(zhí)行過程中快速找到標(biāo)識符。
但是當(dāng)引擎在代碼中遇見了 eval() 或者 with,無法直到eval()中的字符串參數(shù)如何對作用域進(jìn)行修改,也不知道 with 用來創(chuàng)建新詞法作用域的對象的內(nèi)容到底是什么。因?yàn)閑val() 和 with 是在運(yùn)行時(shí)修改或創(chuàng)建新的詞法作用域,所以這會影響引擎在編譯階段的性能優(yōu)化,會導(dǎo)致程序運(yùn)行變慢。
2. 嚴(yán)格模式下嚴(yán)格模式下,eval()在運(yùn)行時(shí)有自己的詞法作用域,而with則被禁用了。
3. eval() 函數(shù)不安全如果你用 eval() 運(yùn)行的字符串代碼被惡意方(不懷好意的人)修改,您最終可能會在您的網(wǎng)頁/擴(kuò)展程序的權(quán)限下,在用戶計(jì)算機(jī)上運(yùn)行惡意代碼
4. with的弊端
with使用'with'可以減少不必要的指針路徑解析運(yùn)算。(但是很多情況下,也可以不使用with語句,而是使用一個(gè)臨時(shí)變量來保存指針,來達(dá)到同樣的效果)with語句使得程序在查找變量值時(shí),都是先在指定的對象中查找。所以那些本來不是這個(gè)對象的屬性的變量,查找起來將會很慢with語句使得代碼不易閱讀,同時(shí)使得JavaScript編譯器難以在作用域鏈上查找某個(gè)變量,難以決定應(yīng)該在哪個(gè)對象上來取值
四、函數(shù)作用域和塊作用域
第三節(jié)中指出,詞法作用域是書寫代碼時(shí)的位置來決定的。但是這些詞法作用域時(shí)基于什么的位置來確定的呢?JavaScript中主要具有函數(shù)作用域和塊作用域兩種。
4.1 函數(shù)作用域
簡單來說,函數(shù)作用域就是指,屬于這個(gè)函數(shù)的全部變量都可以在整個(gè)函數(shù)范圍內(nèi)使用及復(fù)用(在嵌套的作用域中也可以使用)。但是外部作用域無法訪問函數(shù)內(nèi)部的任何內(nèi)容。
4.2 塊作用域
塊作用域指的是變量和函數(shù)不僅可以屬于所處的作用域,也可以屬于某個(gè)代碼塊(通常指 { .. } 內(nèi)部) (1)用 with 從對象中創(chuàng)建出的塊作用域僅在 with 聲明中而非外部作用域中有效。
(2)JavaScript 的 ES3 規(guī)范中規(guī)定 try/catch 的 catch 分句會創(chuàng)建一個(gè)塊作用域,其中聲明的變量僅在 catch 內(nèi)部有效。
(3)let 關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是 { .. } 內(nèi)部)。換句話說,let為其聲明的變量隱式地了所在的塊作用域。
for 循環(huán)頭部的 let 不僅將 i 綁定到了 for 循環(huán)的塊中,事實(shí)上它將其重新綁定到了循環(huán)的每一個(gè)迭代中,確保使用上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值
(4)const 同樣可以用來創(chuàng)建塊作用域變量,但其值是固定的(常量)。之后任何試圖修改值的操作都會引起錯誤
五、函數(shù)提升和變量提升
在介紹閉包之前,我還要啰嗦幾句,以便后續(xù)更好解釋例題。
5.1 變量聲明提升
對于一段JavaScript代碼。我們可能會認(rèn)為時(shí)從上到下一行一行地去執(zhí)行的,但實(shí)際上并不完全是這樣的。
console.log(a); // 1 var a = 1;
如果程序是從上到下執(zhí)行的話,那么第一行代碼應(yīng)該會拋出ReferenceError,因?yàn)椴]有在這之前并沒有聲明變量a。但實(shí)際上會輸出 undefined ,這是為啥呢?
這要從編譯開始說起了,引擎在解釋JavaScript代碼前會先對其進(jìn)行編譯,編譯階段中的一部分工作就是找到所有的聲明,并用合適的作用域?qū)⑺鼈冴P(guān)聯(lián)起來。所以,包括變量和函數(shù)在內(nèi)的所有聲明都會在任何代碼被執(zhí)行前首先被處理。
所以上述代碼實(shí)際的執(zhí)行順序是:
var a; console.log(a); a = 2;
\color{red}{注意}注意:
- 只有聲明本身會被提升,而賦值或其他運(yùn)行邏輯會留在原地。所以上述代碼
var a = 2中只有var a提升了。 - ES6中新加入的let 和 const 關(guān)鍵字聲明變量時(shí),并不會進(jìn)行變量提升。
5.2 函數(shù)聲明提升
除了變量聲明會提升,函數(shù)聲明也會提升。
foo();
function foo() {
console.log( 1 ); // 1
}如上代碼,實(shí)際的執(zhí)行順序如下:
function foo() {
console.log( 1 ); // 1
}
foo();此外,需要注意的是,只有函數(shù)聲明會提升,函數(shù)表達(dá)式并不會提升。
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};5.3 聲明提升注意點(diǎn)
函數(shù)聲明先提升,然后再變量聲明提升
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );一個(gè)普通塊內(nèi)部的函數(shù)聲明通常會被提升到所在作用域的頂部
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log("a"); }
} else {
function foo() { console.log("b"); }
}var 聲明的是函數(shù)作用域,所以在一個(gè)普通塊內(nèi)部,var的變量聲明也會提升
console.log(a) // undefined
if(false) {
var a = 1;
}
console.log(a) // ReferenceError
function f() {
var a = 1;
}六、閉包
介紹完前面的知識后,終于可以引出主角閉包了,首先看看MDN中對閉包的定義:
一個(gè)函數(shù)和對其周圍狀態(tài)(lexical environment,詞法環(huán)境)的引用捆綁在一起(或者說函數(shù)被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個(gè)內(nèi)層函數(shù)中訪問到其外層函數(shù)的作用域。 —— MDN
好像有點(diǎn)晦澀難懂,再來看看《你不知道的JavaScript上卷》中對閉包的定義:
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時(shí),就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。 —— 《你不知道的JavaScript上卷》
還是先來看兩段代碼吧:
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();基于詞法作用域的查找規(guī)則,函數(shù)bar()可以訪問外部作用域中的變量a。這是閉包嗎?反正我之前認(rèn)為這就是。但是確切來說,這并不是閉包。
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); 這段代碼就很清晰地展示了閉包,當(dāng)foo()執(zhí)行完畢后,通常會銷毀foo的內(nèi)部作用域,但是閉包阻止了這一行為。bar()它擁有涵蓋 foo() 內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以供 bar() 在之后任何時(shí)間進(jìn)行引用。
這個(gè)函數(shù)在定義時(shí)的詞法作用域以外的地方被調(diào)用。閉包使得函數(shù)可以繼續(xù)訪問定義時(shí)的詞法作用域。
所以,無論通過何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外,它都會持有對原始定義作用域的引用,無論在何處執(zhí)行這個(gè)函數(shù)都會使用閉包。
第一段代碼中,bar() 就是在其詞法作用域內(nèi)執(zhí)行的,所以嚴(yán)格來說并不能稱為閉包,因?yàn)椴⒉恍枰?ldquo;記住”詞法作用域。
6.1 例題
既然了解了什么閉包,我們來看看文章開頭的面試題:
var arr = []
for (var i = 0; i < 3; i++) {
arr[i] = function() {
console.log(i);
}
}
arr[0]() // 3
arr[1]() // 3
arr[2]() // 3三個(gè)函數(shù)調(diào)用的結(jié)果都是3,為什么會這樣呢?
首先看for循環(huán)中的var i = 0;,其中聲明的變量 i 是全局作用域的一個(gè)變量,所以在執(zhí)行arr[0]() 、arr[1]()、arr[2]()的時(shí)候,在作用域鏈上查找變量 i 時(shí),最終找到都是全局作用域中的同一個(gè)變量 i。因?yàn)榻?jīng)歷了三次循環(huán),所以 i 的值變成了3。故調(diào)用三個(gè)函數(shù)輸出的值都是3。
那么,怎么去改進(jìn)使得程序由正確的輸出呢?
1. for循環(huán)中使用 let 聲明i
前面提到,for 循環(huán)頭部的 let 將 i 綁定到了 for 循環(huán)的塊中,指出變量 i 在循環(huán)過程中不止被聲明一次,每次迭代都會聲明。隨后的每個(gè)迭代都會使用上一個(gè)迭代結(jié)束時(shí)的值來初始化這個(gè)變量。
var arr = []
for (let i = 0; i < 3; i++) {
arr[i] = function() {
console.log(i);
}
}
arr[0]() // 0
arr[1]() // 1
arr[2]() // 22. 立即執(zhí)行函數(shù)(IIFE)
var arr = []
for (var i = 0; i < 3; i++) {
(function IIFE(i) {
arr[i] = function() {
console.log(i);
}
})(i)
}
arr[0]() // 0
arr[1]() // 1
arr[2]() // 2此外這里再分析一種錯誤的寫法:
var arr = []
for (var i = 0; i < 3; i++) {
var j = i;
arr[i] = function() {
console.log(j);
}
}
arr[0]() // 2
arr[1]() // 2
arr[2]() // 2這也是我改進(jìn)的最初答案,想著使用一個(gè)變量記錄當(dāng)前的i值不就行了。但是結(jié)果并不像我想的那樣,翻閱書籍后,發(fā)現(xiàn)var聲明的作用域是函數(shù)作用域,所以在for循環(huán)塊中的var j = i也會聲明提升。相當(dāng)于j也是一個(gè)全局變量了。最后三個(gè)函數(shù)中查找到的j也相同。
到此這篇關(guān)于JavaScript 中的作用域與閉包的文章就介紹到這了,更多相關(guān)JS作用域與閉包內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS實(shí)現(xiàn)的簡單圖片切換功能示例【測試可用】
這篇文章主要介紹了JS實(shí)現(xiàn)的簡單圖片切換功能,結(jié)合實(shí)例形式分析了javascript結(jié)合時(shí)間函數(shù)定時(shí)觸發(fā)控制圖片的遍歷與切換操作相關(guān)技巧,需要的朋友可以參考下2017-02-02
js實(shí)現(xiàn)的簡潔網(wǎng)頁滑動tab菜單效果代碼
這篇文章主要介紹了js實(shí)現(xiàn)的簡潔網(wǎng)頁滑動tab菜單效果代碼,可實(shí)現(xiàn)簡單的鼠標(biāo)滑過tab標(biāo)簽切換的功能,非常簡單實(shí)用,需要的朋友可以參考下2015-08-08
Google Map API更新實(shí)現(xiàn)用戶自定義標(biāo)注坐標(biāo)
由于工作需要,又要開始看Google Map API 代碼,今天再把我之前的GoogleMap類,又更新了下,加了個(gè)簡單的用戶自定義標(biāo)注坐標(biāo)的功能。看看吧(代碼沒怎么優(yōu)化,別見笑)2009-07-07
使用contextMenu插件實(shí)現(xiàn)Bootstrap table彈出右鍵菜單
如今Bootstrap這個(gè)前端框架已被許多人接受并應(yīng)用在不同的項(xiàng)目中,其中“開發(fā)高效,設(shè)備兼容”的特點(diǎn)表現(xiàn)得非常明顯。這篇文章主要介紹了使用contextMenu插件實(shí)現(xiàn)Bootstrap table彈出右鍵菜單,需要的朋友可以參考下2017-02-02
layer.open 子頁面彈出層向父頁面?zhèn)鬏敂?shù)據(jù)的例子
今天小編就為大家分享一篇layer.open 子頁面彈出層向父頁面?zhèn)鬏敂?shù)據(jù)的例子,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09
基于JavaScript實(shí)現(xiàn)文字超出部分隱藏
這篇文章主要介紹了基于JavaScript實(shí)現(xiàn)文字超出部分隱藏 的相關(guān)資料,需要的朋友可以參考下2016-02-02

