前言
你是否也曾有過這樣的經驗?面試時對方拋出一個看似熟悉的問題,明明心裡知道答案卻也聽過,話到嘴邊卻又變得模糊不清。
當「Hoisting」這個詞出現時,我明明記得它的概念,卻無法清晰地向面試官解釋 var、let 和函式之間的差異。那種明明學過,卻無法表達的感覺真的讓人很痛苦。
因此,我決定紀錄下這篇文章。一方面是為了系統性地整理、鞏固自己的知識希望下次能夠想起;另一方面也希望能分享給所有對 Hoisting 同樣感到有些模糊的朋友。
現在,就讓我們一起來徹底搞懂這個重要的概念吧!
什麼是 Hoisting (提升)?
Hoisting 是 JavaScript 的一種內在行為。簡單來說,JavaScript 引擎在正式執行程式碼前,會先進行「編譯」,在這個階段,引擎會找出所有的變數和函式「宣告」,並將它們「提升」到其所在作用域 (Scope) 的最頂端。
這裡有一個最重要的關鍵點,請務必記住:
只有「宣告 (Declaration)」會被提升,而「賦值 (Assignment / Initialization)」會留在原來的位置。
理解了這點,就掌握了 Hoisting 的一半精髓。
變數的 Hoisting (Variable Hoisting)
變數的提升行為會因為你使用 var、let 還是 const 而有所不同。
1. var 的 Hoisting
使用 var 宣告的變數,其宣告會被提升到作用域頂部,並且會被**自動初始化為 undefined**。
看看這個經典範例:
1 | console.log(myVar); // 輸出:undefined |
為什麼第一次 console.log 會是 undefined 而不是直接報錯 ReferenceError 呢?因為在 JavaScript 引擎眼中,上面的程式碼其實是這樣運作的:
1 | // 引擎解析後的版本 |
2. let 與 const 的 Hoisting (與暫時性死區 TDZ)
一個常見的誤解是「let 和 const 不會提升」。這是不對的!
let 和 const 同樣會被提升,但它們和 var 的關鍵區別在於:它們不會被自動初始化。
從宣告被提升開始,到程式碼執行到該宣告的這一行之前,這個變數處於一個無法被存取的狀態,這個區間被稱為 **暫時性死區 (Temporal Dead Zone, TDZ)**。如果在 TDZ 內試圖存取該變數,就會拋出 ReferenceError。
1 | console.log(myLet); // 拋出 ReferenceError: Cannot access 'myLet' before initialization |
TDZ 的存在讓我們能寫出更嚴謹的程式碼,避免在宣告前就使用未定義的變數。
函式的 Hoisting (Function Hoisting)
函式的提升也分為兩種情況。
1. 函式宣告 (Function Declaration)
使用函式宣告的方式,整個函式(包含名稱和函式主體)都會被完整提升。這就是為什麼我們可以在宣告一個函式之前就呼叫它。
1 | sayHello(); // 輸出: "Hello, Hoisting!" |
2. 函式表達式 (Function Expression)
當你把一個函式賦值給一個變數時,這就是函式表達式。它的提升行為會遵循變數 Hoisting 的規則。
1 | // 使用 var |
在上面的例子中,只有變數 sayGoodbye 的宣告被提升並初始化為 undefined。當你試圖執行 undefined() 時,自然會得到一個 TypeError。
如果換成 let,則會因為 TDZ 而拋出 ReferenceError。
優先級:函式 > 變數
如果一個變數和一個函式同名,誰的優先級更高?答案是:函式宣告。
1 | console.log(myFunc); // 輸出: [Function: myFunc] |
從結果可以看出:
- 在編譯階段,
function myFunc和var myFunc都被提升了。但函式的優先級更高,所以在第一個console.log時,myFunc是函式。 - 在執行階段,程式碼跑到
myFunc = "I am a variable"時,這個已經存在的myFunc被重新賦值,所以第二個console.log就變成了字串。
結論與最佳實踐
讓我們快速總結一下 Hoisting 的重點:
- **
var**:宣告被提升,並初始化為undefined。 - **
let,const**:宣告被提升,但不會初始化,形成 TDZ。 - 函式宣告:整個函式被完整提升,優先級最高。
- 函式表達式:行為與其使用的變數 (
var/let/const) 宣告一致。
為了避免 Hoisting 帶來的混亂或許可以這麼做:
- **優先使用
let和const**:它們的 TDZ 特性可以幫助你在開發階段就捕捉到潛在錯誤。 - 保持良好習慣:始終在使用任何變數或函式之前進行宣告和定義。
- 理解原理:死記規則不如理解背後的運作原理,這能幫助你在遇到複雜情境時做出正確判斷。
希望這篇文章能幫助你或我徹底釐清 Hoisting 的觀念!