使用var宣告的變數跟我們一般的認知有很大的不同,主要的原因是抬升及其作用域的特性,使得開發者在使用的時候一不小心就會有誤用的情形,在ES2015後,我們多了let及const的宣告方式,沒有抬升也有了區塊作用域,讓我們擁有更加強固的變數宣告能力,本章會介紹let的相關特性。
作用域的範圍
let
的作用域是在區塊內而非像var
是在整個函式或是全域中,這就是所謂的區塊級別的宣告,區塊級宣告會存在於:
- 宣告變數的函式中。
- 區塊中(
{}
之間的區域)。
第一點是var
所宣告的變數也會滿足的條件,兩個的差別在第二點,如果在一個區塊中(ex: if
)宣告變數,let
的變數在離開區塊時就會被消滅,所以在區塊外是不能使用這個變數的,可是var
因為Hoisting的影響,所以會被抬升至函式頂端,導致在其他區塊也可以使用。
下面這個例子演示var的作用域範圍:
1 | function varDemo(condition){ |
a
的作用域是整個varDemo
函式。b
的作用域因為Hoisting抬至varDemo
函式的頂端,所以作用域還是整個varDemo
函式,但是初始值的設定還是在原本的位置,所以其他地方叫用b
會是undefined
。
我們用上面完全相同的例子,但改用let宣告變數:
1 | function letDemo(condition){ |
c
的作用域跟var
的a
一樣都是整個函式。- 因
let
沒有Hoisting,所以d
的作用域只在if
的區塊中。
禁止重複宣告
var
可以對同一個變數重複宣告,可是對let
來說是非法的:
1 | var ab = 'A'; |
但是如前一節所提,在不同的作用域下的let
變數會是不同的,所以不會有錯誤:
1 | var cd = 'C'; |
因為let
禁止重複宣告,所以在使用switch
時要特別小心,來看一下在MDN中的例子:
1 | let x = 1; |
這個switch
的例子因為在case
裡沒有加上{}
來限制foo
變數的作用域,導致foo
的作用域變成switch
本身,所以case 0
時已經宣告的foo
在case 1
中又宣告了一次,因而產生錯誤。
上面的錯誤示範只要將case
以{}
包住即可:
1 | let x = 1; |
Temporal Dead Zone(TDZ)
JavaScript的編譯器在發現變數宣告有可能會有兩個動作:
- 遇到
var
宣告時,將變數抬升至作用域頂端。 - 遇到
let
或是const
宣告時,將變數**放至Temporal Dead Zone(TDZ)**。
var
會將變數的宣告抬升至作用域頂端,所以在宣告前叫用變數是不會出錯的,只會拿到未宣告的初始值undefined
。
1 | function hoistingDemo(){ |
而由於let
及const
並沒有抬升的機制,所以在區塊裡還未執行到宣告的行數前,這個變數都是在TDZ中,在這個範圍內叫用此變數的話會拋出ReferenceError
的例外。
1 | function TDZDemo(){ |
從上面的程式可以看出來未宣告的b
叫用typeof
會回傳undefined
,而用let
宣告的變數a
卻在叫用typeof
的時候丟回ReferenceError
的例外,這是typeof
為了辨識未宣告變數及在TDZ中的變數所做的差異。
接著來看幾個在MDN上的特別例子。
- 同一個陳述式中
1 | function test(){ |
這裡會拋出ReferenceError
的例外,是不是出乎意料呢? 其實仔細想想(foo + 55)
已經在if
的區塊中了,所以會先找if
區塊中是否有宣告foo
變數,而if
區塊中確實有let foo
的宣告,所以會抓到if
區塊中的foo
,但是這個foo
在(foo + 55)
執行時還是在TDZ中,所以就會拋出錯誤。
- 迴圈中
1 | function go(n) { |
這裡的(let n of n.a)
已經在for的區塊內了,所以會先找for
區塊中有沒有n
的宣告,發現區塊中的確有let n
的宣告,但是在叫用n.a
的時候,在for
區塊內的n
還未被宣告,因此是在TDZ中,所以會拋出ReferenceError
例外。
迴圈中的let
在迴圈中如果想要用index
設定函式的參數時,需要用IIFE來存入副本變數,否則每次的參考變數都會指向同一個。
1 | function varForDemo() { |
而let
可以幫我們很優雅地解決這個問題,因為let
在每個區塊中都會是新的變數,所以每次的迭代都會是不同的變數。
1 | function letForDemo() { |
結語
let
跟var
最大的差別是在作用域的大小,相對於var
是在整個函式或是全域中,let
是在區塊內,而且不會有Hoisting的動作,也禁止重複宣告的限制,讓JavaScript的特性更接近我們所熟知的語言。
在宣告let
變數的同一個區塊中只要還沒有執行到宣告變數的行前,此變數都是在TDZ中,而TDZ會使typeof
拋出ReferenceError
的錯誤。
迴圈中使用var
宣告的變數放入函式的都會指向相同變數,導致我們在之後叫用函式時會產生非預期結果,使用IIFE或是用let
宣告變數讓傳入的變數為新的實體即可解決此問題。