JavaScript - Hoisting 提升

JavaScript - Hoisting 提升

·

3 min read

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 vv = 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 vv = 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

轉換步驟:

  1. 因為有傳入參數,因此先在 VO 中放入 v 並且將值設定為 10

  2. 裡面原本有的變數宣告 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 確實提升了

Ref