0%

深入淺出 JavaScript Hoisting (提升):從 var 到 let 與 const

前言

你是否也曾有過這樣的經驗?面試時對方拋出一個看似熟悉的問題,明明心裡知道答案卻也聽過,話到嘴邊卻又變得模糊不清。

當「Hoisting」這個詞出現時,我明明記得它的概念,卻無法清晰地向面試官解釋 varlet 和函式之間的差異。那種明明學過,卻無法表達的感覺真的讓人很痛苦。

因此,我決定紀錄下這篇文章。一方面是為了系統性地整理、鞏固自己的知識希望下次能夠想起;另一方面也希望能分享給所有對 Hoisting 同樣感到有些模糊的朋友。

現在,就讓我們一起來徹底搞懂這個重要的概念吧!

什麼是 Hoisting (提升)?

Hoisting 是 JavaScript 的一種內在行為。簡單來說,JavaScript 引擎在正式執行程式碼前,會先進行「編譯」,在這個階段,引擎會找出所有的變數和函式「宣告」,並將它們「提升」到其所在作用域 (Scope) 的最頂端。

這裡有一個最重要的關鍵點,請務必記住:

只有「宣告 (Declaration)」會被提升,而「賦值 (Assignment / Initialization)」會留在原來的位置。

理解了這點,就掌握了 Hoisting 的一半精髓。

變數的 Hoisting (Variable Hoisting)

變數的提升行為會因為你使用 varlet 還是 const 而有所不同。

1. var 的 Hoisting

使用 var 宣告的變數,其宣告會被提升到作用域頂部,並且會被**自動初始化為 undefined**。

看看這個經典範例:

1
2
3
console.log(myVar); // 輸出:undefined
var myVar = 10;
console.log(myVar); // 輸出:10

為什麼第一次 console.log 會是 undefined 而不是直接報錯 ReferenceError 呢?因為在 JavaScript 引擎眼中,上面的程式碼其實是這樣運作的:

1
2
3
4
5
6
// 引擎解析後的版本
var myVar; // 1. 宣告被提升到頂部,並被初始化為 undefined

console.log(myVar); // 2. 此時 myVar 是 undefined
myVar = 10; // 3. 賦值的動作留在原地
console.log(myVar); // 4. 此時 myVar 才是 10

2. letconst 的 Hoisting (與暫時性死區 TDZ)

一個常見的誤解是「letconst 不會提升」。這是不對的!

letconst 同樣會被提升,但它們和 var 的關鍵區別在於:它們不會被自動初始化

從宣告被提升開始,到程式碼執行到該宣告的這一行之前,這個變數處於一個無法被存取的狀態,這個區間被稱為 **暫時性死區 (Temporal Dead Zone, TDZ)**。如果在 TDZ 內試圖存取該變數,就會拋出 ReferenceError

1
2
console.log(myLet); // 拋出 ReferenceError: Cannot access 'myLet' before initialization
let myLet = 20;

TDZ 的存在讓我們能寫出更嚴謹的程式碼,避免在宣告前就使用未定義的變數。

函式的 Hoisting (Function Hoisting)

函式的提升也分為兩種情況。

1. 函式宣告 (Function Declaration)

使用函式宣告的方式,整個函式(包含名稱和函式主體)都會被完整提升。這就是為什麼我們可以在宣告一個函式之前就呼叫它。

1
2
3
4
5
sayHello(); // 輸出: "Hello, Hoisting!"

function sayHello() {
console.log("Hello, Hoisting!");
}

2. 函式表達式 (Function Expression)

當你把一個函式賦值給一個變數時,這就是函式表達式。它的提升行為會遵循變數 Hoisting 的規則。

1
2
3
4
5
6
7
// 使用 var
console.log(sayGoodbye); // 輸出: undefined
sayGoodbye(); // 拋出 TypeError: sayGoodbye is not a function

var sayGoodbye = function() {
console.log("Goodbye!");
};

在上面的例子中,只有變數 sayGoodbye 的宣告被提升並初始化為 undefined。當你試圖執行 undefined() 時,自然會得到一個 TypeError

如果換成 let,則會因為 TDZ 而拋出 ReferenceError

優先級:函式 > 變數

如果一個變數和一個函式同名,誰的優先級更高?答案是:函式宣告

1
2
3
4
5
6
7
8
9
console.log(myFunc); // 輸出: [Function: myFunc]

var myFunc = "I am a variable";

function myFunc() {
console.log("I am a function");
}

console.log(myFunc); // 輸出: "I am a variable"

從結果可以看出:

  1. 在編譯階段,function myFuncvar myFunc 都被提升了。但函式的優先級更高,所以在第一個 console.log 時,myFunc 是函式。
  2. 在執行階段,程式碼跑到 myFunc = "I am a variable" 時,這個已經存在的 myFunc 被重新賦值,所以第二個 console.log 就變成了字串。

結論與最佳實踐

讓我們快速總結一下 Hoisting 的重點:

  1. **var**:宣告被提升,並初始化為 undefined
  2. **let, const**:宣告被提升,但不會初始化,形成 TDZ。
  3. 函式宣告:整個函式被完整提升,優先級最高。
  4. 函式表達式:行為與其使用的變數 (var/let/const) 宣告一致。

為了避免 Hoisting 帶來的混亂或許可以這麼做:

  • **優先使用 letconst**:它們的 TDZ 特性可以幫助你在開發階段就捕捉到潛在錯誤。
  • 保持良好習慣:始終在使用任何變數或函式之前進行宣告和定義。
  • 理解原理:死記規則不如理解背後的運作原理,這能幫助你在遇到複雜情境時做出正確判斷。

希望這篇文章能幫助你或我徹底釐清 Hoisting 的觀念!