關於 JavaScript 的提升 (Hoisting)


Posted by Kai on 2020-10-29

一開始在學 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 的時候,出現了另外兩個宣告變數的方式,分別是 letconst。這時候,就產生了一些變化。

function test() {
    console.log(a)
    let a = 2
}
test()

相同的程式碼,只是將宣告的方法改成了 let,執行後就會跳出錯誤訊息說 a is not defined 而方法改成 const 也會是同樣的結果,那是表示說 letconst 是沒有提升嗎?

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 的錯誤訊息,所以 letconst 還是有提升,就像下方的程式碼,只是表現方式和 var 不同。

let a = 1
function test() {
    let a 
    console.log(a)
    a = 2
}
test()

而這裡又必須提到一個名詞叫做,暫時死區 (Temporal Dead Zone, TDZ),這個區域會在使用 letconst 的時候出現,並存在於宣告及賦值之間 (let aa = 2),只要在這個區域中想要存取該變數,就會出現 is not defined 的錯誤訊息。也就是說,當我們使用 letconst 時,想要取用該變數,就必須在賦值之後才能取用到。

以上就是關於 hoisting 的一些資訊,大部分算是課程的筆記 (JavaScript 全攻略:克服JS 的奇怪部分、進階 JavaScript:那些你一直搞不懂的地方),有任何錯誤的地方也麻煩提醒我一下,非常感謝~


#javascript #hoisting







Related Posts

當 React web app 遇上 Progressive web app

當 React web app 遇上 Progressive web app

F2E合作社|麵包屑|Bootstrap 5網頁框架開發入門

F2E合作社|麵包屑|Bootstrap 5網頁框架開發入門

JS30 Day 23 筆記

JS30 Day 23 筆記


Comments