JavaScript 初學者必學觀念:什麼是 Hoisting(提升)?

Published October 14, 2024 by 徐培鈞
JavaScript

學習 JavaScript 時,你可能曾經遇過這樣的神奇情況:一個函式或變數,明明寫在後面,卻可以在前面就被使用。

我們來看一段實際程式碼:

sayHi(); // 👉 這裡先呼叫函式

function sayHi() {
  console.log("Hello!");
}

照理說,我們應該先寫出 sayHi 這個函式的內容,才能在後面呼叫它。

但這段程式碼卻能正常執行,結果會印出:

Hello!

再看一個變數的例子:

在日常寫程式的邏輯中,我們通常會預期「一個變數在使用之前必須先定義」,否則就應該報錯。

例如這段程式碼:

console.log(username); 

let username = "Alice";

這段會報錯:

ReferenceError: Cannot access 'username' before initialization

這就符合多數人的直覺:我還沒說這個變數是什麼,當然不能先拿來用。

同樣的,如果你用 const 宣告也是一樣會報錯:

console.log(age); // ❌ ReferenceError

const age = 18;

但奇怪的是,換成 var,結果竟然不一樣!

現在我們把 let 改成 var,什麼都沒多改,結果卻變得很奇怪:

console.log(message); 

var message = "Hello JavaScript!";

這段程式竟然可以執行,而且還印出了:

undefined

明明變數 message 是在後面才定義的,為什麼在前面就能用了?而且還不是錯誤,而是一個 undefined 的結果。

這些「超自然現象」其實不是魔法,而是 JavaScript 的一個設計機制 —— Hoisting(提升)

在這篇文章中,我們會一步步解釋:

  • 什麼是 Hoisting?
  • 哪些東西會被提升?
  • 提升的過程實際是怎麼運作的?
  • 函式宣告與表達式有什麼不同?
  • varletconst 的提升行為有什麼陷阱?

什麼是 Hoisting?一種「預先搬運」的機制

在 JavaScript 中,當你執行一段程式時,其實程式碼不是直接一行一行從上跑到下那麼簡單。

事實上,在真正開始執行之前,JavaScript 會先進行一個重要的準備階段,稱為 編譯階段(Compilation Phase)

在這個階段,JavaScript 解譯器會預先掃描整份程式碼,並將某些特定的宣告(例如函式或 var 變數)「自動搬到程式的最上面」,這個動作就叫做:

🔁 Hoisting(提升)

簡單說,Hoisting 就是 JavaScript 在背景中偷偷做的一個「搬家動作」:把宣告提前,讓你可以「先用後寫」。

生活化比喻:

想像你早上要出門上班或上學。

雖然你 8:00 才會穿鞋出門,但你通常在一開始準備時就會先把鞋子、鑰匙、手機錢包放在門口的固定位置,不是到要出門的那一刻才臨時找東找西。

這就是一種「事前準備」的行為:你知道之後會用到什麼東西,雖然用的時間在後面,但你會提早把它們先準備好、放好位置。

JavaScript 的 Hoisting 也是類似的邏輯 —— 在程式正式開始執行前,JavaScript 會先把你宣告的函式或變數名稱「提前放到正確的位置」,等真正需要的時候就可以馬上拿來用,不會手忙腳亂。

程式範例說明:

假設你寫了這樣一段程式碼:

sayHello();

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

💡 初學者直覺會以為:

「我還沒寫 sayHello() 的內容,怎麼可以先呼叫它?應該會出錯吧?」

但實際上這段程式會順利執行,印出:

Hello world!

JavaScript 背後其實做了這件事:

在進入執行階段之前,JavaScript 自動把 function sayHello() 搬到最前面,變成這樣:

// JavaScript 心中其實這樣處理:
function sayHello() {
  console.log("Hello world!");
}

sayHello(); // 現在就變得合理了!

你看到的程式碼和 JavaScript 實際「心裡」理解的執行順序,其實是不同的,這就是 Hoisting 的效果。

為什麼 JavaScript 需要 Hoisting?

你可能會覺得奇怪:為什麼 JavaScript 要把「函式或變數搬到最前面」?

不能像其他語言一樣,寫什麼就照順序跑嗎?

這樣不但容易懂,也比較符合直覺。

其實 JavaScript 的這種「先準備再執行」的設計,是有原因的,而且它的出發點其實是為了讓你——寫程式更輕鬆、更不容易出錯

背景補充:JavaScript 原本是設計給「非工程師」用的

JavaScript 在 1995 年剛被發明時,它的定位並不是像 Java 那樣的「嚴謹程式語言」,而是:

✅ 一種輕量、簡單、讓網頁互動更方便的小工具語言

換句話說,它本來就是為了「不懂程式的人也能快速上手」而設計的。所以它很多機制都偏向寬容、彈性,也允許你用比較自由的方式撰寫程式。

所以 Hoisting 的存在,目的有三個:

讓你可以先寫「邏輯」,後面再補「細節」

你可以先寫下你想做的流程,像這樣:

startGame(); // 先寫主流程

function startGame() {
  // 遊戲邏輯
}

這樣的寫法很像在講故事:先講發生什麼事,再慢慢說細節怎麼做。這樣對人來說比較好讀,對寫程式的新手也更友善。

讓函式定義集中管理,程式架構更清晰

當你寫的程式越來越大時,你可能會希望:

  • 上面是主要的邏輯流程
  • 下面集中放所有工具函式(例如:格式化字串、檢查輸入、顯示錯誤訊息)

因為有 Hoisting,你可以把函式通通集中寫在最下面,整體邏輯就不會被打斷,也比較好維護:

main(); // 主流程在上面

function main() {
  sayHi();
  validateInput();
}

function sayHi() {
  console.log("Hi!");
}

function validateInput() {
  // ...
}

🤔 那為什麼 Hoisting 有時會讓人感到困惑?

因為 不是所有的東西都會被提升得一樣

函式宣告會整個被提升,但變數只提升「名稱」不提升「值」,而 letconst 根本不提升(或進入暫時性死區),所以容易出現「前面可以用、但又不是你想像中的用法」的情況。

換句話說:Hoisting 是貼心,但也有些潛規則需要你搞懂。

小結

重點說明不用太在意順序,先用再定義也行
重點說明可以先寫主流程,函式放後面
重點說明預先準備好變數與函式,不會因順序亂掉
重點說明不同宣告類型提升行為不同,要搞懂差異

會被提升的東西:var

當你使用 var 宣告變數時,JavaScript 會在程式一開始先建立變數的「殼」,但不會給它值。

這會導致你在變數賦值之前使用它,雖然不會報錯,但值是 undefined

🧪 範例:

console.log(name); // 👉 undefined

var name = "Alice";

實際上 JavaScript 背後是這樣理解的:

var name;           // 提升了這一行
console.log(name);  // 👉 undefined,因為還沒賦值
name = "Alice";

📌 這種行為容易誤導人,以為變數有值,其實只是存在但還沒被賦值。

會被提升的東西:函式宣告(Function Declaration)

這是 Hoisting 最「完整」的情況 —— 整個函式連同名稱與內部邏輯都會被提前處理好,所以你可以放心在定義之前就使用它。

🧪 範例:

greet(); // 👉 可以正常執行

function greet() {
  console.log("Hello!");
}

這段程式在執行時就像是:

function greet() {
  console.log("Hello!");
}

greet(); // 👉 現在看起來就合理了

📌 這種寫法非常適合在主流程中先呼叫函式、再在下面統一管理定義。

不會被提升的東西:let / const

在文章前面,我們提過,大多數人對程式的直覺是這樣的:

「變數要先宣告,才能使用,不然應該會報錯。」

其實這種直覺是合理的,而且後來 JavaScript 也確實為了貼近這種直覺,在 ES6(2015)中推出了新的變數宣告方式:letconst

這兩種方式的最大特點,就是它們不會像 var 一樣偷偷幫你把變數提到前面用

你什麼時候宣告,它就什麼時候才能用。

這樣一來,程式行為就會更清楚、也更符合人的邏輯。

🧪 範例:這段會報錯是「合理」的

console.log(age); // ❌ ReferenceError

let age = 25;

執行結果會出現錯誤:

ReferenceError: Cannot access 'age' before initialization

JavaScript 的意思是:「我知道你有 age,但你還沒正式宣告完成,現在不准碰。」

這段變數不能用的區域,叫做「暫時性死區(TDZ)」

在 JavaScript 裡,如果你用 letconst 宣告變數,有一個很特別的規則:

不能在宣告那一行之前使用它,不然就會直接報錯!

來看這段簡單的例子:

console.log(name); // ❌ 會報錯!

let name = "小明";

這裡會報:

ReferenceError: Cannot access 'name' before initialization

🤔 為什麼會這樣?

因為 JavaScript 其實一開始就知道你「等一下」會宣告 name

但它規定:「你還沒正式說出來之前,我不准你用!」

這段「變數已經存在,但還不能用」的期間,就叫做:

暫時性死區(TDZ, Temporal Dead Zone)

🏪 更生活化的比喻:變數就像一間店,還沒營業不能進去!

想像你走到一家早餐店門口,店員都已經在裡面備料、打掃、準備開店了。

雖然你看得出來「這家店已經在裡面動起來了」,但只要鐵門還沒拉起來,你就是不能走進去點餐。

這就像 letconst 宣告的變數:

變數已經在「程式的範圍」裡,但只要還沒正式宣告完成,就完全不能碰,碰了就錯。

這段「店裡有人但你不能進」的時間,就是我們說的:

暫時性死區(TDZ, Temporal Dead Zone)

等到鐵門一拉起(程式執行到 letconst 那一行)。

你才可以開始使用那個變數,就像你進店點餐一樣自然。

JavaScript 對應行為JavaScript 已經知道變數存在
JavaScript 對應行為還沒執行到那行 let / const 宣告
JavaScript 對應行為變數還不能使用,提前使用會報錯
JavaScript 對應行為執行到宣告那行後,變數才可以被正常使用

🧪 程式碼範例:

來看這段程式碼:

{
  console.log(food); // ❌ 這裡還不能用

  let food = "壽司"; // ✅ 現在才正式宣告,可以用了
  console.log(food); // 👉 輸出:壽司
}

這是一個區塊作用域(由 {} 包起來的區塊),我們在裡面用 let 宣告了一個變數 food

🔍 這段程式會怎麼執行?

  1. JavaScript 進入這個 {} 區塊時,會先「記住」你等等會宣告一個叫 food 的變數。
  2. 但在這之前(也就是還沒跑到 let food = ... 那一行之前),你就先用 console.log(food) 想讀取 food
  3. 這時候 JavaScript 會直接報錯,因為你正在存取一個還沒啟用的變數。

🧠 白話解釋這段發生了什麼事:

你可以把整段流程想像成:

JavaScript:「我知道你之後會講 ‘food’,但你現在還沒正式說明 ‘food’ 是什麼。你現在就想用它?不行,會出事。」

只有等到這一行真的被執行:

let food = "壽司";

JavaScript 才會「正式啟用」這個變數,讓你在那之後自由使用它。

🎁 對應回我們剛剛說的「暫時性死區(TDZ)」

{} 這個區塊中,變數 food 是在作用域裡沒錯,但從進入區塊那一刻開始,到 let food = ... 這行被執行為止,food 都處於「還不能用」的狀態 ——

這段時間,就叫做「暫時性死區」。

只要你在這段期間嘗試去用它,不論是印出來、比大小,甚至只是問問它是不是存在,JavaScript 都會直接報錯。

為什麼 JavaScript 要設計 TDZ?

「既然變數還不能用,那幹嘛要先知道它存在?為什麼不等跑到那一行再處理就好?」

這是很多初學者學到 let / const 和 TDZ(暫時性死區)時最常問的問題。

乍看之下,這種機制好像多此一舉,但它其實是為了 讓 JavaScript 的執行模型更一致、更嚴謹,也更安全好維護

作用一:在作用域中預留位置,但先「鎖住」變數

很多人以為 JavaScript 是「從上往下一行行執行」的直譯語言,但其實不是。

在真正執行前,JavaScript 引擎會做一個隱藏的動作叫做:

編譯階段(Compile Phase)

這時候,JavaScript 會先掃一遍你的整份程式碼,記錄下哪些地方定義了變數、函式、區塊、作用域等等。

然後才進入 執行階段(Runtime Phase),一行一行執行你的程式。

當你用 letconst 宣告變數時,JavaScript 在編譯階段就會知道:

「喔,這邊會有個變數 x,我先幫你預留記憶體位置。」

不過它不會馬上幫你設定值,也不讓你提早拿來用。

這段「知道它會出現,但還不能用」的期間,就是暫時性死區(TDZ)

可以能會想問:為什麼不乾脆「等執行到那一行再知道」就好?

原因在於,若變數直到跑到那行宣告才「出現」,那整個區塊的變數狀況會變得不穩定。

想像你進入一間教室,老師說:「今天會有 5 位同學上台報告。」

這樣你從一開始就知道有哪些人會出現。

但如果老師改成:「走到講台再看有沒有人」,你每一分鐘都會緊張地想:「到底會不會有人上來?會有誰?」

這樣就會讓整個流程混亂、不確定、難以安排。

JavaScript 也是這樣:一進區塊,就先把變數名列出來(雖然還不能用)

這樣的行為讓 JavaScript 在一進到一段區塊時(像是一個函式或 if 區塊),就能提前知道有哪些變數會出現,雖然它們還沒被賦值,但至少「知道它們會來」。

來看個例子:

function test() {
  console.log(x); // ❌ ReferenceError:這裡知道有 x,但還不能用!

  if (true) {
    let x = 5;
  }
}

這段的行為是:

  1. JavaScript 一開始就知道 x 會在 if 裡被宣告(它會被放進 scope 裡)
  2. 但在 let x = 5 這一行之前,x 還不能用(進入 TDZ)
  3. 所以前面 console.log(x) 就會報錯,而不是靜靜地印出 undefined
🔍 如果沒有 TDZ,會發生什麼事?

如果 JavaScript 是「走到哪才知道哪有變數」,那前面 console.log(x) 就會變成以下幾種不確定狀況:

  • 有時候 x 存在,有時候根本沒宣告 → 很難預測
  • 有些情況會印出 undefined,有些又報錯 → 開發者容易混亂
  • 程式維護時不清楚作用域內到底有哪些變數 → 很難 debug

這就像你寫一部小說,如果每段內容裡的角色都可能忽然冒出來又不見,讀者會很痛苦。

✅ TDZ 的設計,讓程式像一本規劃清楚的劇本

TDZ 保證一件事:

「只要變數用 letconst 宣告,它一定會在區塊一開始就被記錄下來,但你只能在它被賦值後再使用。」

這就像劇本中的角色設定:

  • 一開場就把角色名列出來(但還沒出場)
  • 出場順序照著劇情來
  • 誰先講話、誰後出現,全都規劃好

這種結構讓 JavaScript 更有秩序、可預測,也讓你更容易寫出乾淨、容易維護的程式碼

作用二:阻止你誤用還沒準備好的變數

在舊版 JavaScript(使用 var)中,如果你在變數尚未真正設定好值之前就使用它,程式不會報錯,只是給你一個 undefined。但這其實很危險,因為你以為變數沒值,結果只是太早用了

🧪 舊寫法(容易出 bug):
if (status === "ready") {
  startGame();
}

var status = "ready";

你可能以為這段程式碼會順利啟動遊戲,但實際上不會,因為:

  • status 雖然已經被「提升」,但值還是 undefined
  • 所以 status === "ready"false
  • startGame() 根本不會被執行!

而且這整段程式不會報錯,你會以為哪裡壞掉了,但找不到問題

🛡️ 現在用 let + TDZ,錯誤會馬上跳出來:
if (status === "ready") {
  startGame();
}

let status = "ready";

這段就會直接報錯:

ReferenceError: Cannot access 'status' before initialization

JavaScript 引擎在你使用 let 宣告變數時,會在程式「一開始」就知道你打算宣告 status,但直到那一行真正執行之前,它會「鎖住」這個變數,不讓你亂用。

作用三:讓瀏覽器更容易做效能最佳化

JavaScript 並不是完全從上往下直接執行的語言,它在開始跑程式之前,會先經歷一個編譯階段,做以下事情:

  • 掃描你的程式碼
  • 建立作用域(Scope)環境
  • 記下有哪些變數和函式會出現
  • 幫每個變數預留空間
  • 判斷哪些東西什麼時候可以被使用

如果你使用 letconst 宣告變數,JavaScript 就會在「一開始」把它們標記在對應的作用域中,即使值還沒設定,它也會記下:

「這個區塊裡會出現某個變數,但要等到第幾行才正式啟用。」

這種「先知道、再初始化」的設計,讓 JavaScript 有更多機會提前準備,也更能有效率地使用記憶體與資源。

🚀 TDZ 幫助的底層優化有哪些?
  1. 記憶體配置更有效率
    因為引擎一開始就知道有哪些變數會用到,就可以提早幫你配置好記憶體位置,不需要等到中途才緊急找空間。
  2. 靜態分析更精確
    編譯階段可以先掃出所有變數的範圍與壽命(什麼時候會出現、用到哪裡),對像 Babel、Terser 這種壓縮工具來說,也能更有效壓縮沒用到的變數。
  3. 產生更快的執行碼
    引擎根據作用域與變數資訊,可以提早編排執行流程(例如做 inlining、移除不必要的變數查找等),整體程式跑得更快。
🧠 一個簡單比喻:排隊入場 vs 現場亂找人

想像你在參加一場大型活動:

  • 如果主辦單位在進場前就先對名單、座位安排、流程動線做足準備(就像 JavaScript 的編譯階段),整場活動會非常順暢。
  • 如果主辦單位什麼都沒規劃,等到人來了才臨時決定(像是等變數出現再處理),那現場就會一團混亂,效率低落。

TDZ 就像是這個名單規劃機制:

「先知道你會出現,等你報到之後再放你進場。」

這樣做,既能避免意外亂入,也能幫整體流程提速、減少出錯。

明白了,我來幫你用更白話、更口語的方式重新整理這兩段內容,讓初學者也能輕鬆理解:

不會被提升的東西:函式表達式(Function Expression)

你可能會寫過這樣的程式:

sayHi(); // ❌ TypeError: sayHi is not a function

var sayHi = function () {
  console.log("Hi!");
};

錯誤解釋

這裡報的是 TypeError,原因是:

  • JavaScript 在「編譯階段」會先把 var sayHi 這個變數的名字搬到最上面
  • 所以你在 sayHi(); 那一行呼叫它時,這個變數是「有的」,不會說 undefined
  • 但是值還沒被指定,也就是 sayHi 裡面還是 undefined
  • 然後你試圖執行 undefined(),這才是錯的

就像你說:「我知道有個東西叫 sayHi,但它現在不是個可以執行的函式」,所以會報出:

TypeError: sayHi is not a function

不會被提升的東西:箭頭函式(Arrow Function)

箭頭函式寫法比較簡潔,看起來像是函式,但它本質上跟剛剛那個函式表達式一樣 —— 是「把一個函式當作值,丟進變數裡」。

add(2, 3); // ❌ ReferenceError: Cannot access 'add' before initialization

const add = (a, b) => a + b;

錯誤解釋:為什麼會出現 ReferenceError

來看一次這段程式碼:

add(2, 3); // ❌ ReferenceError: Cannot access 'add' before initialization

const add = (a, b) => a + b;

這裡 JavaScript 直接報錯,說:

ReferenceError: Cannot access 'add' before initialization

意思就是說:

你想用一個變數,但這個變數還沒『初始化』,所以不能用。

什麼是「初始化」?

你可以這樣理解:「初始化」其實就是:

變數準備好了,裡面有值了,可以開始使用了。

像這樣的程式碼:

const name = "Amy";

JavaScript 在處理這一行的時候,其實會分兩個步驟來進行:

  1. 宣告(declare):JavaScript 會先知道你接下來要用一個變數,叫做 name
    (但此時變數還沒有值,還不能使用)
  2. 初始化(initialize):JavaScript 會將 "Amy" 這個值放進 name 這個變數裡。
    (這時候變數才真的準備好,可以開始被你使用)

簡單說,就是:

  • 宣告:告訴 JavaScript 變數會存在(但還沒有值)
  • 初始化:把值真正放進變數裡,正式啟用

🔥 所以剛才為什麼會報錯?

回到你原本的程式碼:

add(2, 3); // ❌ ReferenceError: Cannot access 'add' before initialization

const add = (a, b) => a + b;

你在第一行的時候就嘗試呼叫 add 這個變數,但這時候:

  • add 變數確實已經「宣告」了(JavaScript 已經知道你會用這個名字)
  • 但它還沒「初始化」,也就是裡面根本沒有值
  • 所以 JavaScript 馬上阻止你使用,直接報錯,避免你使用一個尚未準備好的變數。

就像你準備了一個空杯子,但裡面還沒倒飲料,朋友就急著拿起來喝,當然不行!

延伸補充:「那用 var 宣告的變數為什麼不會報錯?」

我們再一次用剛才的例子,但這次改成用 var

add(2, 3); // ❌ TypeError: add is not a function

var add = (a, b) => a + b;

你會發現,這一次錯誤訊息改變了,變成:

TypeError: add is not a function

而不是剛才的 ReferenceError

🔍 為什麼變成 TypeError?

原因是這樣的:

var 宣告的變數在 JavaScript 執行前的「提升」過程中,會做兩件事:

  1. 宣告 這個變數名稱
  2. ✅ 同時自動幫這個變數 初始化(initialize)成 undefined

也就是說,程式實際開始執行前,變數已經存在,而且裡面已經先放入了一個 undefined 的值

當你呼叫:

add(2, 3);

這一行的時候,JavaScript 引擎會這樣理解:

「我確定已經有一個變數叫 add,而且裡面已經有個值了,但這個值現在是 undefined。」

你試著把這個 undefined 當成函式來呼叫(undefined()),當然就會出錯:

TypeError: add is not a function

📌 與剛才 const 宣告的差異比較:

宣告階段完成?✅ 完成
初始化階段完成?✅ 已完成(值是 undefined)
提前使用會發生什麼事?存取到 undefined,但無法當成函式呼叫
錯誤類型❌ TypeError
宣告階段完成?✅ 完成
初始化階段完成?❌ 未完成(沒值)
提前使用會發生什麼事?完全禁止存取,直接報錯
錯誤類型❌ ReferenceError

白話總結一下:

  • var 宣告變數時,變數會自動被初始化成 undefined,因此你提前存取它不會有問題(但若你把它當函式用,就會報 TypeError)。
  • letconst 時,變數會被鎖起來,連存取的機會都沒有,直接報錯 ReferenceError,告訴你「這個變數還不能碰!」

這樣設計就是為了讓你盡早發現錯誤,不要默默使用錯誤的值而不自知。

JavaScript 提升(Hoisting)總結小抄

在 JavaScript 中,有些變數或函式可以在宣告前就直接使用,有些卻不行。

這張小抄幫你一次搞懂差別在哪裡:

會被提升?✅ 會提升
提升的內容有哪些?整個函式(名稱 + 裡面的內容)
能不能在宣告前使用?✅ 可以放心用
初學者要注意的地方最直覺、安全,不容易踩雷。
會被提升?✅ 會提升
提升的內容有哪些?只有變數名稱,值是 undefined
能不能在宣告前使用?⚠️ 可以用,但小心!
初學者要注意的地方提前使用時只會得到 undefined,容易搞錯。
會被提升?❌ 不會提升
提升的內容有哪些?不會提升(但名稱被鎖在暫時性死區 TDZ)
能不能在宣告前使用?❌ 不行,會直接報錯!
初學者要注意的地方因為存在 TDZ(暫時性死區),使用前一定要先宣告才安全。
會被提升?❌ 不會提升
提升的內容有哪些?不會提升(實質是變數的賦值)
能不能在宣告前使用?❌ 不行,會報錯!
初學者要注意的地方本質上是「把函式當成值」,和一般變數一樣無法提前使用。

function 函式宣告

函式宣告的寫法像這樣:

hello(); // ✅ 可以正常運作!

function hello() {
  console.log("Hi!");
}

為什麼能用?
因為 JavaScript 一開始就會幫你把整個函式(包含內容)搬到程式最上面,所以你怎麼用都很安全、很直覺。

var 變數宣告

var 宣告的變數只會「一半提升」:

console.log(name); // ⚠️ 印出 undefined(沒錯誤,但危險!)

var name = "Amy";

為什麼不會報錯?

因為 JavaScript 只幫你提升了 name 這個變數名稱,並且先自動給它一個預設值 undefined

但這也會有問題,你可能誤以為它有值,結果只是空的。

letconst 變數宣告

letconst 宣告時,就完全不會被提升:

console.log(age); // ❌ ReferenceError(完全不能用!)

let age = 18;

為什麼直接報錯?

雖然 JavaScript 知道你會宣告這個變數,但它故意把變數鎖起來,不讓你提前用(暫時性死區,TDZ),這是為了避免你犯錯,提前抓 bug。

函式表達式或箭頭函式

函式表達式或箭頭函式的本質,其實是把函式當作值放進變數:

sayHi(); // ❌ TypeError: sayHi is not a function

var sayHi = function() {
  console.log("Hi!");
};

add(2, 3); // ❌ ReferenceError: Cannot access 'add' before initialization

const add = (a, b) => a + b;

為什麼這樣用會報錯?

因為只有變數名稱可能會提升(如果你用 var),但函式的內容絕對不會提前設定進去。

  • 如果用 var 宣告,你會拿到 undefined,當成函式執行就變成 TypeError
  • 如果用 constlet 宣告,它還在暫時性死區(TDZ),你完全不能碰它,直接跳 ReferenceError

用一句話記住差別

  • function 宣告:JavaScript 幫你完整準備好函式,最安全
  • var 宣告:只準備了一半 (undefined),很容易誤用
  • let / const 宣告:完全不能提早使用,最安全但最嚴格
  • 函式表達式/箭頭函式:函式當成值來看待,無法提前使用,必須先宣告再用

這樣你之後看到任何狀況,都能輕鬆分辨、不再搞錯囉!

結語

Hoisting 是 JavaScript 中的一個特殊行為,讓程式可以在邏輯上更彈性安排,但同時也容易讓初學者踩雷。

只要掌握這三個原則,你就能安全又有效地撰寫程式:

  1. 想用提升?就用函式宣告。
  2. 避免 var,多使用 let / const
  3. 不要在宣告前就使用變數或函式表達式。

記住這些,你就能在撰寫 JavaScript 程式時避免常見錯誤,邁向進階之路!