JavaScript 觀念 - 提升
ES6 前後的 Hoisting 觀念筆記。
甚麼是提升?
「提升(Hoisting)」是 JavaScript 特有的一種現象,它的意思是當一個變數或函式在被宣告之前就可以被使用,並且不會出現錯誤,另外 ES6 以前都是使用 var
來宣告變數,與 ES6 新增的 let
、const
在提升行為會也有所不同。
ES6 以前的提升
1 |
|
以上針對 x
取值,但是在這之前並沒有宣告一個名為 x
的變數,因此會找不到該變數而回報錯誤,下面是正常的做法:
1 |
|
這樣就是一個正常的流程,程式碼會從第一行開始由上往下執行,所以在使用變數之前需要先宣告變數,以確保這個變數是存在的。
接著下面嘗試把第 1、2 行位置進行對調:
1 |
|
按照前面的說法,上述程式碼應該會回報錯誤,但是得到的結果卻是 undefined
,而這就是提升所造成的現象,感覺像是 var x
這段程式碼被提升到所有程式碼之前。
大多數的程式語言中,變數在被使用之前是需要先宣告的,但是 JavaScript 可以在變數宣告之前就使用該變數,不過關於提升還有一些注意事項,就是會被提升的只有宣告的行為,值的指派並不會被提升,以下面程式碼為例:
1 |
|
範例跟前面的相同,只是多了一個指派數值的動作,而最後也印出預期的結果,這個時候如果再將 1、2 行位置進行對調,印出的結果會是 7
嗎?
1 |
|
答案是 undefined
,照理來說宣告的變數會被提升,可是得到的結果卻不是 7
,原因其實就如前面提到的,會被提升的只有宣告的動作,而範例中宣告的動作指的是 var x
,後方的 = 7
屬於值的指派,並不會跟著宣告一起被提升。
但是結果為甚麼會是 undefined
?原因在於 JavaScript 在開始執行你撰寫的程式碼之前,會先把所有宣告的變數、一般函式都預留一個記憶體空間,但不會馬上指派值給變數,這個預留記憶體空間的動作就是提升,到這邊為止屬於「創造階段」,而這個階段結束之後,變數才會被賦值,這個賦值的過程則是「執行階段」,undefined
就是變數在創造階段建立記憶體空間時,預設給定的初始值。
可以將上面的程式碼運作流程理解成以下形式:
1 |
|
目前為止已經知道會被提升的只有宣告的動作,但是除了變數之外,一般的函式宣告也會被提升,以下面程式碼為例:
1 |
|
嘗試在函式被宣告之前呼叫,也可以順利執行且不會回報錯誤,因為這是一般的函式宣告(具名函式),整個函式都會被提升,因此就可以在函式宣告之前呼叫。
那匿名函式就不會被提升嗎?答案是會,下面嘗試將範例改成以匿名函式的方式建立:
1 |
|
結果出現錯誤了,不過是不是與 var
宣告的變數很像?其實概念是一樣的,這裡的程式碼確實有被提升,但是被提升的只有變數的宣告 var fn
,函式的資料在執行階段才會指派給變數 fn
,此時的 fn
的值為 undefined
,而 undefined
並非函式因此呼叫的行為就會回報錯誤。
雖然無法呼叫,但是能透過變數取值來驗證上述說法:
1 |
|
而程式碼實際運作流程就像下面這樣:
1 |
|
總結來說,JavaScript 中的「提升(Hoisting)」指的是宣告變數的提升,而值的賦予並不會提升;此外,提升並不會變更程式碼的位置,只是感覺像是整段程式碼被移動到最上方。
let、const 的提升
關於 let
、const
有沒有提升行為,一開始我自己也是透過以下方法來作結論的:
1 |
|
原本看到上面的結果,是認為 let
、const
沒有提升行為的,直到看到一篇文章寫了下面這段程式碼:
1 |
|
可以看到在函式 fnA
裡面,嘗試在變數 a
被宣告之前取值,照理來說,如果 let
沒有提升行為,第 3 行的變數 a
應該會指向到函式外層的變數 a
,因此印出結果應該是 1
才對,但是最終卻得到 Cannot access 'a' before initialization
的錯誤訊息,而這就證明了 let
是有提升行為的,只是不允許在變數宣告之前被存取。
而前面提到 var
在提升時,也就是創造階段預設會給定初始值 undefined
,而 let
、const
則不會,所以在變數實際賦值前嘗試存取就會出現上面的錯誤,而提升後到賦值之前的這一個區間,稱為「暫時性死區(TDZ)」。