JavaScript - Hoisting 提升

Hoisting 是怎麼發生的?
變數和函數的宣告會在編譯階段就被放入記憶體,但實際位置和程式碼中完全一樣。
從這段 MDN 對於 hoisting 的說明大概可以了解到,Javascript 在執行程式碼之前會先進行編譯,而在編譯的過程中會將變數宣告以及函式宣告提升 (hoist) 到該 scope 的頂端,但需注意這邊並非實際改動程式碼的位置。
JS 在運作時是分成「編譯」和「執行」兩個步驟。而 hoisting 是發生在編譯的階段。
JS 在編譯的階段會將變數及函式的宣告處理好(hoist 流程下方有補充說明)並加入到 scope 中,在執行的階段去使用它。
為什麼會需要 Hoisting
在執行程式碼前,JavaScript 會把函式宣告放進記憶體裡面。這樣在即使在宣告函示之前就先呼叫它,程式碼仍然可以運作
白話文就是我們可以在 function 宣告前就先呼叫它
catName("Chloe");
function catName(name) {
console.log("My cat's name is " + name);
}
/*
上面程式的結果是: "My cat's name is Chloe"
*/
這樣做的好處是:
不需要把 function 宣告放在每個檔案的最上方 (avoid painful bottom-up ML-like order)
不同 function 可以互相呼叫 (mutual recursion)
什麼是提升 hoisting
變數的提升
使用還沒宣告的變數,會發生錯誤 ReferenceError: a is not defined
console.log(a) // ReferenceError: a is not defined
使用該變數後才宣告,則會是 undefined
console.log(a) // undefined
var a
第二行的
var a被「提升」到了最上面程式碼的位置並沒有真的移動
變數的「宣告」會提升,「賦值」則不會
console.log(a) // undefined
var a = 5
將 var a = 5 拆成「宣告」跟「賦值」兩個部分,只有變數的宣告 var a 會被提升,但賦值 a = 5 並不會
var v = 5
var v
console.log(v) // 答案是 5 不是 undefined
同理,這邊我們將 var v = 5 拆成 var v 跟 v = 5 ,因為宣告會提升、賦值不會,所以上述程式碼可以看成:
var v
var v
v = 5
console.log(v)
函式的提升
function 的宣告也會提升,而且「優先權比較高」
console.log(a) // [Function: a]
var a
function a () {}
有參數傳入的 function
function test(v){
console.log(v)
var v = 3
}
test(10)
答案是 10 而不是 undefined。
雖然我們依照先前提到的將 var v = 3 拆成 var v 與 v = 3 ,並且 function 中的變數宣告 var v 被提升了,但因為 function 有參數傳入,按照 function 的 hoisting 規則其實會變成這個樣子:
function test(v){
var v = 10 // 下面呼叫 test(10),參數傳入,值為 10
var v // 已經有 v 這個屬性,因此原本的變數宣告被忽略
console.log(v)
v = 3
}
test(10)
// 答案是 10
轉換步驟:
因為有傳入參數,因此先在 VO 中放入
v並且將值設定為 10裡面原本有的變數宣告
var v則因為步驟 1 已經有v這個屬性了,所以忽略不管
此時的 VO :
{
v: 10
}
function 的 hoisting 是怎麼運作的
這篇「我知道你懂 hoisting,可是你了解到多深?」講得滿清楚的,以下是我看完文章的簡單筆記:
function 的 execution context (EC) 與 variable object (VO)
每個 function 需要的資訊會存在一個對應的 execution context (EC)
每個 EC 會有相對應的 variable object (VO):有點像是 function 的記憶體,執行 function 需要取值的資訊都會存在這個物件中
該 VO 裡面找不到的資訊,就會透過 scope chain 繼續往上找,最後找不到的話就會報錯
On entering an execution context, the properties are bound to the variable object in the following order
這邊提到在進入 EC 的時候,會按照底下的執行流程把資訊放到 VO:
1. VO 中對於「參數」的宣告
參數會直接被放到 VO 中
參數沒有值的話,它的值會被初始化成
undefined
function test(a, b, c) {}
test(10)
此時該 function 的 VO:
{
a: 10,
b: undefined,
c: undefined
}
2. VO 中對於「function」 的宣告
VO 裡新增一個屬性,值就是 function 回傳的東西
如果 VO 已經有同名的屬性,就把它覆蓋掉
function test(a){
function a(){} // test(1) 傳入
}
test(1)
此時該 function 的 VO:
{
a: function a // 原本的參數 a 被覆蓋掉了
}
3. VO 中對於「變數」 的宣告
VO 裡面新增一個屬性並且把值設為
undefined如果 VO 已經有這個屬性的話,值不會被改變
let, const 與 hoisting
let 看起來沒有 hoisting:
console.log(a) // ReferenceError: a is not defined
let a
但實際上卻是:
var a = 10
function test(){
console.log(a)
let a
}
test() // ReferenceError: a is not defined
如果 let 沒有 hoisting,答案應該會是 10,但答案卻是 ReferenceError: Cannot access 'a' before initialization ,代表 let 確實提升了


