一開始在學 JS 時候,通常都是使用 var 來宣告變數,所以有時候就會出現以下的程式碼:
function test() {
console.log(a)
var a = 2
}
test()
這時候,會輸出的是 undefined
而不是跳出錯誤訊息說 a is not defined
,其中 前者是表示 a = undefined
而後者則是表示 a
這個變數並沒有被宣告所以不存在。
但依照程式一行一行執行的觀念,此時的 a
應該是沒有被宣告的才對,那 undefined
是怎麼來的呢?而這個就是我們常聽到的提升 (hoisting) 的概念。
而在講提升之前,必須要大概了解一下,在我們執行 JS 的程式碼時,發生了什麼。這邊會在關係到其他兩個名詞,一個是 Execution Contexts (執行環境),另一個則是 Execution Stack (執行堆)
執行環境
簡單來說就是一個 {} 所包圍的部分。每個執行環境中,都預設有一個 Variable Object (VO)
,而當變數或函式被宣告的時候,就會在 VO 中添加 properties,假如宣告的變數在還沒賦值之前則會給予預設值 undefined
,相同的變數則會進行覆蓋的動作。而執行程式碼時,第一個建立的環境會是 Global Execution Context (全域執行環境)。
執行堆
執行堆則是一個排隊等待的地方(?,簡單來說,每當一個執行環境被建立時,就會堆疊在舊的執行環境上。當所有的執行環境都被建立完成後,JS 就會開始依序執行執行環境中的內容,執行完後則換下一個執行環境,一直到所有執行環境的內容都被執行完畢。
function a() {
b()
console.log(3)
}
function b() {
c()
console.log(2)
}
function c() {
console.log(1)
}
a()
以上面的程式碼為例,最先被創造的會是全域執行環境,再來會是被呼叫的 a()
,接著是 b()
最後是 c()
,也就是說執行堆的由上至下分別是 c()
=> b()
=> a()
=> 全域執行環境
,所以執行這段程式碼後,依序會 log 出 1 2 3。但必須要注意的是,當把 log 的程式碼放到了呼叫函式的程式碼前面,log 的程式碼會先被執行。
也就是說,在每個執行環境中,會依照順序由上往下執行程式碼,而遇到了另一個執行環境時,則會創造一個新的執行環境 (執行堆往上堆疊),執行完畢後才會回到原本的環境繼續執行剩餘的程式碼。
提升
講這麼多,提升到底是怎麼發生的呢?其實從執行環境的初始化就可以看到一些端倪了。
假如宣告的變數在還沒賦值之前則會給予預設值
undefined
也就是當執行環境創造出來的時候,會先找有沒有變數宣告,假如有會先用 undefined
去當成變數的預設值,之後再去執行環境中的程式碼。
function test() {
console.log(a)
var a = 2
}
test()
所以再看一次這段程式碼,當 test()
這個執行環境被創造,發現有 a
這個變數被宣告,因此在還沒賦值 var a = 2
之前,a
會是預設值 undefined
。所以 log 出來的就會是預設值 undefined
。
提升的順序
那提升有順序可言嗎?
答案是有的,我們可以用下面函式來驗證。
function test() {
console.log(a)
function a() {}
var a = 2
}
test()
當執行這段程式碼的時候,不管前後順序,輸出的 a
都會是一個函式,也就是說函式的提升順序是高於變數的。
function test(a) {
console.log(a)
function a() {}
var a = 2
}
test(123)
接下來,假如我們在函式中帶入參數呢?執行的結果依然會是一個函式,但假如將函式那段程式碼移除,結果就會變成 123
。這樣一來我們就可以排出提升的順序是:函式 => 函式的參數 => 變數宣告,這樣的結果。
ES6 的逆襲
以上的結果都是在使用 var
宣告變數的情形,但在 ES6 的時候,出現了另外兩個宣告變數的方式,分別是 let
跟 const
。這時候,就產生了一些變化。
function test() {
console.log(a)
let a = 2
}
test()
相同的程式碼,只是將宣告的方法改成了 let
,執行後就會跳出錯誤訊息說 a is not defined
而方法改成 const
也會是同樣的結果,那是表示說 let
跟 const
是沒有提升嗎?
var a = 1
function test() {
console.log(a)
var a = 2
}
test()
以上這段程式碼執行後,會因為 var
的提升而輸出 undefined
,而當我們將函數內的 var a = 2
移除,就會輸出 1
let a = 1
function test() {
console.log(a)
let a = 2
}
test()
但當我們把 var
改成 let
的時候,得到的依然是 a is not defined
的錯誤訊息,所以 let
跟 const
還是有提升,就像下方的程式碼,只是表現方式和 var
不同。
let a = 1
function test() {
let a
console.log(a)
a = 2
}
test()
而這裡又必須提到一個名詞叫做,暫時死區 (Temporal Dead Zone, TDZ),這個區域會在使用 let
及 const
的時候出現,並存在於宣告及賦值之間 (let a
和 a = 2
),只要在這個區域中想要存取該變數,就會出現 is not defined
的錯誤訊息。也就是說,當我們使用 let
與 const
時,想要取用該變數,就必須在賦值之後才能取用到。
以上就是關於 hoisting 的一些資訊,大部分算是課程的筆記 (JavaScript 全攻略:克服JS 的奇怪部分、進階 JavaScript:那些你一直搞不懂的地方),有任何錯誤的地方也麻煩提醒我一下,非常感謝~