欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JavaScript?中的作用域與閉包

 更新時間:2022年05月26日 15:53:49   作者:??大力yy????  
這篇文章主要介紹了JavaScript中的作用域與閉包,JavaScript是一種具有函數(shù)優(yōu)先的輕量級,解釋型或即時編譯型的編程語言,下文是更多相關介紹需要的小伙伴可以參考一下

前言:

前幾天面試中,面試官拋出一道題,問我輸出結(jié)果是啥:

var arr = []
for (var i = 0; i < 3; i++) {
    arr[i] = function() {
        console.log(i);
    }
}
arr[0]()
arr[1]()
arr[2]()

無知的我脫口而出:“三個2”,面試官眉頭一皺:“你再仔細看看”。哦豁,大事不妙,趕緊仔細看看,不對是三個3。面試官點了點頭,心想應該是答對了。接著面試官又問我,怎么修改呢。心想這不就是閉包嗎,var改let嗎。接著面試官又問我其他方法呢,我說用立即執(zhí)行函數(shù)。結(jié)果到了手寫的時候突然懵了,背的八股文忘了,然后就尷尬了。。。
看來只背背八股文還是不行,所以今天針對這個問題,仔細學習了一下前因后果。

一、JavaScript 是一門編譯語言

通常 JavaScript 被歸類于“解釋性語言”或“腳本語言”等,作為開發(fā)Web 頁面的腳本語言而出名。但是事實上,它是一門編譯語言
MDN對JavaScript的定義如下:

JavaScript (JS) 是一種具有函數(shù)優(yōu)先的輕量級,解釋型或即時編譯型的編程語言。
JavaScript 是一種基于原型編程、多范式的動態(tài)腳本語言,并且支持面向?qū)ο?、命令式和聲明式(如函?shù)式編程)風格。
—— MDN

1.1 傳統(tǒng)編譯語言的編譯步驟

(1)分詞/詞法分析(Tokenizing/Lexing)將由字符組成的字符串分解成(對編程語言來說)有意義的代碼塊,這些代碼塊被稱為詞法單元(token)。
(2)解析/語法分析(Prsing)將詞法單元流(數(shù)組)轉(zhuǎn)換成一個由元素逐級嵌套所組成的代表了程序語法結(jié)構(gòu)的樹。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree,AST)。
(3)代碼生成將 AST 轉(zhuǎn)換為可執(zhí)行代碼。這個過程與語言、目標平臺等息息相關;例如window下C語言編譯最終得到.exe文件。

1.2 JavaScript 與傳統(tǒng)編譯語言的區(qū)別

(1)JavaScript 與傳統(tǒng)編譯語言不同,它不是提前編譯的,編譯結(jié)果也不能在分布式系統(tǒng)中移植。 (2)JavaScript引擎負責整個JavaScript程序的編譯及執(zhí)行過程,編譯器負責語法分析及代碼生成等,相對于傳統(tǒng)編譯語言的編譯器更加復雜(例如:在語法分析和代碼生成階段有特定的步驟來對運行性能進行優(yōu)化)
(3)大部分情況下JavaScript 編譯發(fā)生在代碼執(zhí)行前的幾微秒(甚至更短)時間內(nèi)。

二、作用域(Scope)

作用域:負責收集并維護由所有標識符(變量)組成的一系列查詢,并實施一套非常嚴格的規(guī)則,確定當前執(zhí)行的代碼對這些標識符的訪問權(quán)限。—— 《你不知道的JavaScript 上卷》

在了解什么是作用域前,首先來看看var a = 2;是如何進行處理的??赡艽蟛糠趾臀乙粯诱J為這就是一句聲明,但是JavaScript認為這里面又兩個完全不同的聲明,一個由編譯器在編譯時處理,另一個由引擎在運行時處理。

  • 首先編譯器會將這段程序分解成詞法單元,然后將詞法單元解析成一個樹結(jié)構(gòu)
  • 緊接進行代碼生成,編譯器會進行如下處理:
    • 遇到 var a,編譯器會詢問作用域是否已經(jīng)有一個該名稱的變量存在于同一個作用域的集合中。如果是,編譯器會忽略該聲明,繼續(xù)進行編譯;否則它會要求作用域在當前作用域的集合中聲明一個新的變量,并命名為 a。
    • 接下來編譯器會為引擎生成運行時所需的代碼,這些代碼被用來處理 a = 2 這個賦值操作。引擎運行時會首先詢問作用域,在當前的作用域集合中是否存在一個叫作 a 的變量。如果是,引擎就會使用這個變量;如果否,引擎會繼續(xù)查找該變量(即在作用域鏈上查找)如果引擎最終找到了 a 變量,就會將 2 賦值給它。否則引擎就會舉手示意并拋出一個異常!

2.1 LHS查詢 和 RHS查詢

如上例子,編譯器為引擎生成了為引擎生成了運行時所需的代碼后,引擎執(zhí)行它時,是如何查找變量a的呢?這里就要引入LHS查詢和RHS查詢兩個術語了。
(1)LHS 查詢:試圖找到變量的容器本身,從而可以對其賦值
(2)RHS 查詢:查找某個變量的值
以下面程序為例,對LHS 和 RHS 做更深一步的解釋:

function foo(a) {
    console.log( a ); // 2
}
foo( 2 );
  • 首先:foo() 函數(shù)的調(diào)用,需要對foo進行RHS查詢,即查找 foo 的值
  • 緊接著執(zhí)行foo(2)時,這里傳遞參數(shù)時,隱式進行了 a = 2,那么這里需要對 a 進行LHS查詢,找到a后再將2賦值給a。
  • 進入foo函數(shù)內(nèi)部,然后對console進行RHS查詢,然后對a進行RHS查詢,傳遞進log()。

再看個例子:

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ù)嵌套在另一個塊或函數(shù)中時,就發(fā)生了作用域的嵌套。
因此,在當前作用域中無法找到某個變量時,引擎就會在外層嵌套的作用域中繼續(xù)查找,直到找到該變量,或抵達最外層的作用域(即:全局作用域)為止。

function foo(a) {
    console.log( a + b );
}
var b = 2;
foo( 2 ); // 4

例如上述代碼中,對b進行RHS查詢時,無法在當前函數(shù)foo的作用域中完成,需要向上一級作用域查找,即在全局作用域中完成了。
LHS查詢和RHS查詢都會在當前執(zhí)行的作用域開始查找,如果沒有找到,則會向上一級查找,直到查找成功或者達到全局作用域。達到全局作用域,無論是否找到,都會停止查詢過程。

2.3 ReferenceError 和 TypeError

若在任何作用域中都無法查找到變量,那么引擎就會拋出異常。但是針對LHS查詢失敗和RHS查詢失敗拋出的異常是不同的。

(1)ReferenceError

console.log(a);

上述代碼在執(zhí)行時,會拋出 ReferenceError 。這是因為在對 a 進行RHS查詢時,是無法查找到改變量的。這是因為變量 a ”未聲明“,不存在于任何作用域中。
所以,RHS 查詢在所有嵌套的作用域中遍尋不到所需的變量,引擎就會拋出 ReferenceError異常。
相比之下,LHS查詢在所有嵌套的作用域中查詢不到目標變量時,全局作用域會創(chuàng)建一個具有該名稱的變量,并將其返還給引擎。但是,如果是在”嚴格模式“下,引擎也會拋出 ReferenceError.

// 嚴格模式下
"use strict"
a = 2;  // ReferenceError
// 非嚴格模式下
a = 2  // 執(zhí)行成功

即:

  • RHS查詢失敗時:引擎會拋出 ReferenceError
  • LHS查詢失敗時
    • 嚴格模式: 引擎會拋出 ReferenceError;
    • 非嚴格模式:全局作用域會創(chuàng)建一個具有該名稱的變量,并將其返還給引擎

(2)TypeError

如果 RHS 查詢找到了一個變量,但是你嘗試對這個變量的值進行不合理的操作,比如試圖對一個非函數(shù)類型的值進行函數(shù)調(diào)用,或著引用 null 或 undefined 類型的值中的屬性,那么引擎會拋出另外一種類型的異常,叫作 TypeError。

// 對非函數(shù)類型的值進行調(diào)用
let a = 0;
a();

// 引用undefined類型的值的屬性
let b;
b.name;

(3)ReferenceError 和 TypeError 的區(qū)別

ReferenceError 表示RHS查詢失敗,或嚴格模式下的LHS查詢失敗
TypeError 則代表RHS查詢成功了,但是對結(jié)果的操作是非法或不合理的。

小結(jié)

作用域是一套規(guī)則,用于確定在何處以及如何查找變量(標識符)。如果查找的目的是對變量進行賦值,那么就會使用 LHS 查詢;如果目的是獲取變量的值,就會使用 RHS 查詢。

LHS 和 RHS 查詢都會在當前執(zhí)行作用域中開始,如果沒找到,就會向上級作用域繼續(xù)查找目標標識符,這樣每次上升一級作用域,最后抵達全局作用域(頂層),無論找到或沒找到都將停止。

不成功的 RHS 引用會導致拋出 ReferenceError 異常。不成功的 LHS 引用會導致自動隱式地創(chuàng)建一個全局變量(非嚴格模式下),該變量使用 LHS 引用的目標作為標識符,或者拋出 ReferenceError 異常(嚴格模式下)。

三、詞法作用域

第二節(jié)中提到作用域可以定義為一套規(guī)則,但是這套規(guī)則又是如何去定義的呢?
作用域主要有兩種主要的工作模型:詞法作用域 和 動態(tài)作用域,其中 JavaScript 采用的是詞法作用域

3.1 詞法階段

如第一節(jié)中介紹的,大部分標準語言編譯器的第一個工作階段叫作詞法分析。即對源代碼中的字符進行檢查,識別出每個單詞。
簡單來說,詞法作用域就是定義在詞法階段的作用域。即由代碼中變量的書寫位置來決定的。

3.2 詞法作用域 查找規(guī)則

(1)作用域查找是找從運行時所處的最內(nèi)部作用域開始,逐級向外,直到遇見第一個匹配的標識符為止。
(2)遮蔽效應:在多層嵌套的作用域中可以定義同名的標識符,但是內(nèi)部的標識符會”遮蔽“外部的標識符。
(3)全局變量會自動成為全局對象的屬性。所以可以通過全局對象的引用來間接訪問全局變量。

// a是全局變量
var a = 1;
// 瀏覽器中全局對象一般為window
console.log(window.a)  // 1

所以,當全局變量在內(nèi)部作用域被同名變量“遮蔽”時,可通過該方法訪問到全局變量,例如:

// a是全局變量
var a = 1;

funcion foo() {
    let a = 2;
    console.log(a);  // 2
    console.log(window.a);  // 1
}

但是,對于非全局變量來說,如果被遮蔽了,就無法訪問到。
(4)無論函數(shù)何時、何處以及如何被調(diào)用,它的詞法作用域都只由被聲明時所處的位置決定。(即與代碼中書寫的位置保持一致)
(5)詞法作用域的查找只會查找\color{red}{一級標識符}一級標識符。
例如:針對foo.a.b,詞法作用域只會試圖查找 foo 標識符,找到 foo 這個變量后,對象屬性訪問規(guī)則會接管對 a 和 b 屬性的訪問。
這也解釋了前面當引擎遇到console.log();時,只會對 console 進行一次RHS查詢,不會接著對 log 進行RHS查詢。

3.3 欺騙詞法 —— eval、with

3.2中說到,詞法作用域是由書寫代碼期間函數(shù)所聲明的位置來定義。但是JavaScript中有兩個機制會在運行時“修改”詞法作用域——eval、with。但是很多地方都建議不使用這兩種機制,因為欺騙詞法作用域會導致性能下降。

(1)eval

eval() 是全局對象的一個函數(shù)屬性。
eval() 的參數(shù)是一個字符串。如果字符串表示的是表達式,eval() 會對表達式進行求值。如果參數(shù)表示一個或多個 JavaScript 語句,那么eval() 就會執(zhí)行這些語句。 —— MDN

換個說法,eval可以在書寫的代碼中用程序生成代碼并運行,就好像代碼是寫在那個位置一樣。
在執(zhí)行 eval(..) 之后的代碼時,引擎并不“知道”或“在意”前面的代碼是以動態(tài)形式插入進來,并對詞法作用域的環(huá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; 這段代碼會被當作本來就在那里一樣來處理。因此對foo函數(shù)的詞法作用域進行了修改,在foo函數(shù)內(nèi)部創(chuàng)建了一個變量b,遮蔽了全局變量b,所以輸出 1, 3。
eval(..) 可以在運行期修改書寫期的詞法作用域。但個人覺得其實并沒有破壞詞法作用域的查找規(guī)則,即把 eval() 的參數(shù)在eval書寫的位置替換eval()。然后再按詞法作用域規(guī)則去查找。

\color{red}{注意:}注意:在嚴格模式下,eval()在運行時有自己的詞法作用域,所以其中的聲明無法修改所在的作用域。

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

從上述代碼可以看出,嚴格模式下,在eval()函數(shù)內(nèi)部輸出b,值為3,但是在eval()函數(shù)外部輸出b,值為2.

不推薦使用與 eval() 以及與 eval() 類似的函數(shù)setTimeout(..) 和setInterval(..) 的第一個參數(shù)可以是字符串,字符串的內(nèi)容可以被解釋為一段動態(tài)生成的函數(shù)代碼。這些功能已經(jīng)過時且并不被提倡。(目前一般是傳遞回調(diào)函數(shù))
new Function(..) 最后一個參數(shù)可以接受代碼字符串,并將其轉(zhuǎn)化為動態(tài)生成的函數(shù)(前面的參數(shù)是這個新生成的函數(shù)的形參)。這種構(gòu)建函數(shù)的語法比eval(..) 略微安全一些,但也要盡量避免使用。

(2)with

'with'語句將某個對象添加到作用域鏈的頂部,如果在statement中有某個未使用命名空間的變量,跟作用域鏈中的某個屬性同名,則這個變量將指向這個屬性值。如果沒有同名的屬性,則將拋出ReferenceError異常。—— MDN

with (expression) {
    statement
}

換種說法,with 可以將一個沒有或有多個屬性的對象處理為一個完全隔離的全新的詞法作用域,因此這個對象的屬性也會被處理為定義在這個作用域中的詞法標識符。

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)建了一個全新的詞法作用域,并把 obj 放在該詞法作用域的頂層(若把該詞法作用域類比為全局作用域,那么obj就是一個全局對象)。在該全新的作用域中,obj的所有屬性都可以直接訪問。
console.log(a)輸出1:當前詞法作用域未聲明變量a,所以向上一級查找,obj中包含屬性a,所以輸出1
console.log(b)輸出5:當前詞法作用域中聲明了變量b,該變量b”遮蔽“了obj中的屬性b,所以輸出5
console.log(c)輸出3:當前詞法作用域未定義變量c,obj中也沒有屬性c,則繼續(xù)向全局作用域查找,所以輸出3
console.log(d)拋出異常ReferenceError:因為在當前詞法作用域以及其嵌套的所有詞法作用域中都未聲明變量d,RHS查詢失敗,所以拋出ReferenceError

\color{red}{注意:}注意:在 ECMAScript 5嚴格模式下,with標簽已經(jīng)被禁用。

(3)為什么不推薦使用 eval() 和 with

1. eval() 和 with 對性能的影響JavaScript 引擎會在編譯階段進行數(shù)項的性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進行靜態(tài)分析,并預先確定所有變量和函數(shù)的定義位置,才能在執(zhí)行過程中快速找到標識符。
但是當引擎在代碼中遇見了 eval() 或者 with,無法直到eval()中的字符串參數(shù)如何對作用域進行修改,也不知道 with 用來創(chuàng)建新詞法作用域的對象的內(nèi)容到底是什么。因為eval() 和 with 是在運行時修改或創(chuàng)建新的詞法作用域,所以這會影響引擎在編譯階段的性能優(yōu)化,會導致程序運行變慢。
2. 嚴格模式下嚴格模式下,eval()在運行時有自己的詞法作用域,而with則被禁用了。
3. eval() 函數(shù)不安全如果你用 eval() 運行的字符串代碼被惡意方(不懷好意的人)修改,您最終可能會在您的網(wǎng)頁/擴展程序的權(quán)限下,在用戶計算機上運行惡意代碼
4. with的弊端

  • with使用'with'可以減少不必要的指針路徑解析運算。(但是很多情況下,也可以不使用with語句,而是使用一個臨時變量來保存指針,來達到同樣的效果)
  • with語句使得程序在查找變量值時,都是先在指定的對象中查找。所以那些本來不是這個對象的屬性的變量,查找起來將會很慢
  • with語句使得代碼不易閱讀,同時使得JavaScript編譯器難以在作用域鏈上查找某個變量,難以決定應該在哪個對象上來取值

四、函數(shù)作用域和塊作用域

第三節(jié)中指出,詞法作用域是書寫代碼時的位置來決定的。但是這些詞法作用域時基于什么的位置來確定的呢?JavaScript中主要具有函數(shù)作用域和塊作用域兩種。

4.1 函數(shù)作用域

簡單來說,函數(shù)作用域就是指,屬于這個函數(shù)的全部變量都可以在整個函數(shù)范圍內(nèi)使用及復用(在嵌套的作用域中也可以使用)。但是外部作用域無法訪問函數(shù)內(nèi)部的任何內(nèi)容。

4.2 塊作用域

塊作用域指的是變量和函數(shù)不僅可以屬于所處的作用域,也可以屬于某個代碼塊(通常指 { .. } 內(nèi)部) (1)用 with 從對象中創(chuàng)建出的塊作用域僅在 with 聲明中而非外部作用域中有效。
(2)JavaScript 的 ES3 規(guī)范中規(guī)定 try/catch 的 catch 分句會創(chuàng)建一個塊作用域,其中聲明的變量僅在 catch 內(nèi)部有效。
(3)let 關鍵字可以將變量綁定到所在的任意作用域中(通常是 { .. } 內(nèi)部)。換句話說,let為其聲明的變量隱式地了所在的塊作用域。
for 循環(huán)頭部的 let 不僅將 i 綁定到了 for 循環(huán)的塊中,事實上它將其重新綁定到了循環(huán)的每一個迭代中,確保使用上一個循環(huán)迭代結(jié)束時的值重新進行賦值
(4)const 同樣可以用來創(chuàng)建塊作用域變量,但其值是固定的(常量)。之后任何試圖修改值的操作都會引起錯誤

五、函數(shù)提升和變量提升

在介紹閉包之前,我還要啰嗦幾句,以便后續(xù)更好解釋例題。

5.1 變量聲明提升

對于一段JavaScript代碼。我們可能會認為時從上到下一行一行地去執(zhí)行的,但實際上并不完全是這樣的。

console.log(a);  // 1
var a = 1;

如果程序是從上到下執(zhí)行的話,那么第一行代碼應該會拋出ReferenceError,因為并沒有在這之前并沒有聲明變量a。但實際上會輸出 undefined ,這是為啥呢?
這要從編譯開始說起了,引擎在解釋JavaScript代碼前會先對其進行編譯,編譯階段中的一部分工作就是找到所有的聲明,并用合適的作用域?qū)⑺鼈冴P聯(lián)起來。所以,包括變量和函數(shù)在內(nèi)的所有聲明都會在任何代碼被執(zhí)行前首先被處理

所以上述代碼實際的執(zhí)行順序是:

var a;
console.log(a);
a = 2;

\color{red}{注意}注意:

  • 只有聲明本身會被提升,而賦值或其他運行邏輯會留在原地。所以上述代碼var a = 2 中只有 var a 提升了。
  • ES6中新加入的let 和 const 關鍵字聲明變量時,并不會進行變量提升。

5.2 函數(shù)聲明提升

除了變量聲明會提升,函數(shù)聲明也會提升。

foo();
function foo() {
    console.log( 1 ); // 1
}

如上代碼,實際的執(zhí)行順序如下:

function foo() {
    console.log( 1 ); // 1
} 
foo();

此外,需要注意的是,只有函數(shù)聲明會提升,函數(shù)表達式并不會提升。

foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
    // ...
};

5.3 聲明提升注意點

函數(shù)聲明先提升,然后再變量聲明提升

foo(); // 1
var foo;
function foo() {
    console.log( 1 );
}
foo = function() {
console.log( 2 );

一個普通塊內(nèi)部的函數(shù)聲明通常會被提升到所在作用域的頂部

foo(); // "b"
var a = true;
if (a) {
    function foo() { console.log("a"); }
} else {
    function foo() { console.log("b"); }
}

var 聲明的是函數(shù)作用域,所以在一個普通塊內(nèi)部,var的變量聲明也會提升

console.log(a)  // undefined
if(false) {
    var a = 1;
}
console.log(a)  // ReferenceError
function f() {
    var a = 1;
}

六、閉包

介紹完前面的知識后,終于可以引出主角閉包了,首先看看MDN中對閉包的定義:

一個函數(shù)和對其周圍狀態(tài)(lexical environment,詞法環(huán)境)的引用捆綁在一起(或者說函數(shù)被引用包圍),這樣的組合就是閉包closure)。也就是說,閉包讓你可以在一個內(nèi)層函數(shù)中訪問到其外層函數(shù)的作用域。 —— MDN

好像有點晦澀難懂,再來看看《你不知道的JavaScript上卷》中對閉包的定義:

當函數(shù)可以記住訪問所在的詞法作用域時,就產(chǎn)生了閉包,即使函數(shù)是在當前詞法作用域之外執(zhí)行。 —— 《你不知道的JavaScript上卷》

還是先來看兩段代碼吧:

function foo() {
    var a = 2;
    function bar() {
        console.log( a ); // 2
    }
    bar();
}
foo();

基于詞法作用域的查找規(guī)則,函數(shù)bar()可以訪問外部作用域中的變量a。這是閉包嗎?反正我之前認為這就是。但是確切來說,這并不是閉包。

function foo() {
    var a = 2;
    function bar() {
        console.log( a );
    }
    return bar;
}
var baz = foo();
baz(); 

這段代碼就很清晰地展示了閉包,當foo()執(zhí)行完畢后,通常會銷毀foo的內(nèi)部作用域,但是閉包阻止了這一行為。bar()它擁有涵蓋 foo() 內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以供 bar() 在之后任何時間進行引用。
這個函數(shù)在定義時的詞法作用域以外的地方被調(diào)用。閉包使得函數(shù)可以繼續(xù)訪問定義時的詞法作用域。

所以,無論通過何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外,它都會持有對原始定義作用域的引用,無論在何處執(zhí)行這個函數(shù)都會使用閉包。

第一段代碼中,bar() 就是在其詞法作用域內(nèi)執(zhí)行的,所以嚴格來說并不能稱為閉包,因為并不需要“記住”詞法作用域。

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

三個函數(shù)調(diào)用的結(jié)果都是3,為什么會這樣呢?

首先看for循環(huán)中的var i = 0;,其中聲明的變量 i 是全局作用域的一個變量,所以在執(zhí)行arr[0]() 、arr[1]()、arr[2]()的時候,在作用域鏈上查找變量 i 時,最終找到都是全局作用域中的同一個變量 i。因為經(jīng)歷了三次循環(huán),所以 i 的值變成了3。故調(diào)用三個函數(shù)輸出的值都是3。
那么,怎么去改進使得程序由正確的輸出呢?

1. for循環(huán)中使用 let 聲明i

前面提到,for 循環(huán)頭部的 let 將 i 綁定到了 for 循環(huán)的塊中,指出變量 i 在循環(huán)過程中不止被聲明一次,每次迭代都會聲明。隨后的每個迭代都會使用上一個迭代結(jié)束時的值來初始化這個變量。

var arr = []
for (let i = 0; i < 3; i++) {
    arr[i] = function() {
        console.log(i);
    }
}
arr[0]()  // 0
arr[1]()  // 1
arr[2]()  // 2

2. 立即執(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

這也是我改進的最初答案,想著使用一個變量記錄當前的i值不就行了。但是結(jié)果并不像我想的那樣,翻閱書籍后,發(fā)現(xiàn)var聲明的作用域是函數(shù)作用域,所以在for循環(huán)塊中的var j = i也會聲明提升。相當于j也是一個全局變量了。最后三個函數(shù)中查找到的j也相同。

到此這篇關于JavaScript 中的作用域與閉包的文章就介紹到這了,更多相關JS作用域與閉包內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論