[ES6] 從 var 到 let

函數作用域(Function Scope)與聲明提前(Hoisting)

在一些類似於C語言的編程語言中,每一對花括號 {} 都是一個作用域,變量只在其被聲明時所在的作用域內有效,我們稱之為塊級作用域(Block Scope)

在ES5標準下的Javascript中,沒有塊級作用域,取而代之地使用了函數作用域(Function Scope): 變量在聲明它們的函數體,以及函數體嵌套的任意函數體內都是有定義的。

以上的定義意味著,函數內聲明的所有變量,無論其在函數內聲明的位置,只要在函數體內部,都是可見的,這個特性被稱為聲明提前(Hoisting),即Javascript函數里聲明的所有變量(不包括其賦值)都被提前至函數體的頂部。

注:聲明提前這步操作是在Javascript引擎預編譯時進行的,是在代碼開始運行之前。

[ES6] 從 var 到 let

這種機制的缺點

缺點1:內層變量可能會覆蓋外層變量

請看下面的例子

[ES6] 從 var 到 let

以上代碼的問題在於,即使condition為false的情況下,變量a仍然會被聲明,且可以在else中被訪問到(此時由於未被賦值,其值為undefined)。這樣會在實際的開發中如果不留心,可能導致內層變量會覆蓋外層變量的問題。如:

[ES6] 從 var 到 let

缺點2:for循環中用於計數的變量會洩露為全局變量

for(var i = 0; i < 10; i++){
	//...
}
console.log(i); //10

在此想多介紹一下Javascript中的作用域

ES5標準下Javascript中只有兩種作用域,全局作用域和函數作用域,直觀來說:

[ES6] 從 var 到 let

那麼請看如下代碼:

{
 var a = 0;
}
console.log(a); //0

這裡a雖然在花括號內,但不在函數內部,則依然屬於全局作用域,任何地方均可以訪問到變量a。

那我們再看for循環,在for循環中

[ES6] 從 var 到 let

看清楚,for循環並不是在函數作用域,而是在全局作用域進行的。這樣一來,循環變量就會一直存在於全局作用域中,造成隱患。

只擁有全局作用域和函數作用域而導致的問題隨著Javascript程序規模的擴大日益凸顯出來,於是在ES6標準中,加入了塊作用域,用let關鍵字聲明的變量的作用域為塊作用域。具體來說:

  1. 用let聲明的變量其作用域以花括號為界,即塊作用域(Block Scope)。
  2. 用let聲明的變量不存在聲明提前(Hoisting)。

例子1:

{
 var a = 'A';
 let b = 'B';
}
console.log(a); // A
console.log(b); // ReferenceError: b is not defined

例子2:

function test(){
 console.log(a); //undefined (聲明提前)
 console.log(b); //ReferenceError: b is not defined (不存在聲明提前)
 var a = 'A';
 let b = 'B';
}
test();

在for循環中,計數變量特別適合用let來聲明,避免汙染全局變量。如:(請對比前文使用var的for循環例子)

for(let i = 0; i < 10; i++){
 //...
}
console.log(i); //ReferenceError: i is not defined

臨時死區

再進一步,由於用let聲明的變量不存在聲明提前,在一個變量如果用了let聲明,那麼從當前塊作用域開始處( “{” 位置),到變量聲明處,就形成了一個該變量名的臨時死區(temporal dead zone)。如

function test(){
 
 //變量tag的臨時死區
 let tag = 'B';
}

臨時死區可能會帶來的問題:

[ES6] 從 var 到 let

很明顯,第三行代碼位於tag的臨時死區內。因此對於名字為tag變量的所有操作都會報錯。有些臨時死區造成的錯誤比較隱蔽,如:

[ES6] 從 var 到 let

再比如:

[ES6] 從 var 到 let

不允許重複聲明

let不允許在相同作用域內,重複聲明同一個變量。例如:

[ES6] 從 var 到 let

長久以來,var聲明讓開發者在循環中創建函數變得異常困難,因為變量到了循環之外仍能被訪問的到,例如:

[ES6] 從 var 到 let

我們預期的輸出結果是輸出0-9,但此段代碼實際上輸出了十次數字10。

這是因為循環裡的每次迭代同時共享著變量i,循環內部創建的函數全部保留了對相同變量的引用,循環結束時i的值為10,所以每次調用console.log(i)時都會輸出數字10

------《深入理解ES6》

我們來深究一下這個問題。

console.log(i) 中的 i 是可以被改變的,並不是在函數定義後就固定不變的。

[ES6] 從 var 到 let

Javascript是基於詞法作用域的,詞法作用域的基本規則是:

Javascript函數執行用到了作用域鏈,這個作用域鏈是在 >>>函數定義的時候

什麼意思呢?

當我們定義一個函數時,系統實際保存了:

  1. 函數的定義
  2. (函數定義所在的作用域) 的 所有變量
  3. (函數定義所在的作用域) 的 (父作用域) 的 所有變量
  4. (函數定義所在的作用域) 的 (父作用域) 的 (父作用域) 的 所有變量
  5. ...
  6. 直到最頂層的全局作用域

其中 2~6 形成了一個作用域鏈。我們可以簡單說:

當我們定義一個函數時,系統實際保存了:

  1. 函數的定義
  2. 函數定義時所在的作用域鏈 (再次提醒,是函數定義時,而不是函數執行時)

當函數執行時,用到一個變量,則按照從2至6的順序查找變量名,把最先找到的拿來執行。而如果最後在6(也就是全局變量才查找到)

[ES6] 從 var 到 let

當我們 調用函數A之前,函數A定義之後,第五行的 var i = 3 中的 i 並沒有機會被修改。這是因為每一層函數都形成了一個閉包,即函數內部的變量保存在函數作用域內,外部並訪問不到。

那麼再看如下例子:

[ES6] 從 var 到 let

這裡在 調用函數A之前,函數A定義之後,i 的值可以被任意修改。

這樣我們在回到剛剛的for循環中定義函數的代碼:

[ES6] 從 var 到 let

i是一個在全局作用域的變量,在每次循環結束後都會被加1,聰明的WebStorm知道這是在for循環中容易犯的錯誤,於是拋出警告,告訴你這樣寫會和你本意相悖,輸出十個10,而不是你想要的0,1,2,3,4,5,6,7,8,9。

那麼如何解決這個問題呢?在ES6標準之前,開發者們在函數中使用立即調用表達式(IIFE),強制生成計數器變量的副本,來保證最後輸出結果是0到9:

[ES6] 從 var 到 let

在ES6中,只要使用let關鍵字來聲明循環變量 i ,就可以方便的實現:

[ES6] 從 var 到 let

最後說說const

熟悉其他語言的同學可能會對const並不陌生,在ES6中,它與let的主要區別是:

  1. const聲明一個常量,在聲明時就必須初始化,之後不可更改
  2. 若用const聲明一個對象,對象整體不能修改,但可以修改對象中屬性的值。
[ES6] 從 var 到 let

好了,就先介紹到這裡,覺得不錯別忘了點贊呦,比心❤️。


分享到:


相關文章: