## Hoisting 是怎麼發生的? > **變數**和**函數**的宣告會在編譯階段就被放入記憶體,但實際位置和程式碼中完全一樣。 從這段 [MDN](https://developer.mozilla.org/zh-TW/docs/Glossary/Hoisting) 對於 hoisting 的說明大概可以了解到,Javascript 在執行程式碼之前會先進行編譯,而在編譯的過程中會將變數宣告以及函式宣告提升 (hoist) 到該 scope 的頂端,但需注意這邊**並非實際改動程式碼的位置**。 - JS 在運作時是分成「編譯」和「執行」兩個步驟。而 hoisting 是發生在編譯的階段。 - JS 在編譯的階段會將變數及函式的宣告處理好(hoist 流程下方有補充說明)並加入到 scope 中,在執行的階段去使用它。 ## 為什麼會需要 Hoisting > 在執行程式碼前,JavaScript 會把**函式宣告**放進記憶體裡面。這樣在即使在宣告函示之前就先呼叫它,程式碼仍然可以運作 白話文就是我們可以在 **function 宣告前就先呼叫它** ```javascript 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`** ```javascript! console.log(a) // ReferenceError: a is not defined ``` **使用該變數後才宣告,則會是 `undefined`** ```javascript! console.log(a) // undefined var a ``` - 第二行的 `var a` 被「提升」到了最上面 - 程式碼的位置並沒有真的移動 **變數的「宣告」會提升,「賦值」則不會** ```javascript! console.log(a) // undefined var a = 5 ``` 將 `var a = 5` 拆成「宣告」跟「賦值」兩個部分,只有變數的宣告 `var a` 會被提升,但賦值 `a = 5` 並不會 ```javascript var v = 5 var v console.log(v) // 答案是 5 不是 undefined ``` 同理,這邊我們將 `var v = 5` 拆成 `var v` 跟 `v = 5` ,因為宣告會提升、賦值不會,所以上述程式碼可以看成: ```javascript var v var v v = 5 console.log(v) ``` ### 函式的提升 #### function 的宣告也會提升,而且「優先權比較高」 ```javascript! console.log(a) // [Function: a] var a function a () {} ``` #### 有參數傳入的 function ```javascript! 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 規則其實會變成這個樣子: ```javascript 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 : ```javascript { v: 10 } ``` ### function 的 hoisting 是怎麼運作的 這篇「[我知道你懂 hoisting,可是你了解到多深?](https://blog.techbridge.cc/2018/11/10/javascript-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` ```javascript! function test(a, b, c) {} test(10) ``` 此時該 function 的 VO: ```javascript! { a: 10, b: undefined, c: undefined } ``` #### 2. VO 中對於「function」 的宣告 - VO 裡新增一個屬性,值就是 function 回傳的東西 - 如果 VO 已經有同名的屬性,就把它覆蓋掉 ```javascript! function test(a){ function a(){} // test(1) 傳入 } test(1) ``` 此時該 function 的 VO: ```javascript! { a: function a // 原本的參數 a 被覆蓋掉了 } ``` #### 3. VO 中對於「變數」 的宣告 - VO 裡面新增一個屬性並且把值設為 `undefined` - 如果 VO 已經有這個屬性的話,值**不會**被改變 ## let, const 與 hoisting let 看起來沒有 hoisting: ```javascript! console.log(a) // ReferenceError: a is not defined let a ``` 但實際上卻是: ```javascript! 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 - [我知道你懂 hoisting,可是你了解到多深?](https://blog.techbridge.cc/2018/11/10/javascript-hoisting/)