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 確實提升了