初學者指南:深入了解 JavaScript 的 Call Stack(呼叫堆疊)

Published October 18, 2024 by 徐培鈞
JavaScript

當你在寫程式時,會把一些重複的動作寫成函式,然後在需要的時候叫它幫你做事。

舉例來說:

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

greet("Harry");
console.log("Bye");

這段程式中,電腦的行為就像這樣:

  1. 一開始,電腦會先掃過整個程式碼,先記住有哪些函式(像是 greet)的定義,這叫做建立函式記憶在技術上這步驟屬於「建立階段」或「Hoisting」)。
  2. 接著,程式從 greet("Harry") 這一行開始執行,這是你叫電腦去執行 greet 裡的程式。
  3. 為了執行 greet,電腦會記下現在執行的位置,然後跳去 greet 函式裡,執行 console.log("Hello Harry")
  4. greet 做完後,電腦會回到剛剛中斷的地方的下一行,也就是 console.log("Bye")

為了讓這一切能正確進行,電腦需要一個東西來記住「我剛剛執行到哪裡、等一下要接著做什麼」──這個東西就是呼叫堆疊。

這篇文章會用清楚的步驟幫你理解:

  • 為什麼需要呼叫堆疊?
  • 它在做什麼?
  • 如果出錯(例如無限呼叫)會發生什麼事?

什麼是「呼叫堆疊」?

先從生活情境說起:你是個忙碌的人

想像你今天在家裡做很多事:

你正在 廚房煮泡麵,這時候:

  1. 📞 電話響了,你怕泡麵煮太久,於是先關火,然後拿一張便條紙寫下:

✅「煮泡麵到第2分鐘,水還沒滾完」

  1. 🏃‍♂️ 接起電話,講到一半又有人按門鈴,你怕等一下忘了電話內容,又再拿一張便條紙寫下:

✅「電話講到媽媽說她晚上要來吃飯」

  1. 🛎 接著你去開門,是外送員,簽收完餐點之後,你回來看到便條紙,知道你要回去接著講電話。
  2. 電話講完之後,看回第一張便條紙,知道要回廚房繼續煮泡麵。

你有沒有發現?

每次「暫停一件事去處理另一件事」,你都會在桌上放一張便條紙記錄目前的進度,等處理完,就一張一張往回收拾,回到剛剛中斷的地方繼續。

呼叫堆疊的概念就是這樣來的

在 JavaScript 中,當你呼叫一個函式,電腦就會做一樣的事:

  1. 在呼叫函式之前,把目前程式的位置記起來
  2. 然後「暫停原本的工作」,轉去執行你呼叫的那個函式
  3. 等那個函式執行完,再「根據剛剛的記錄」回來原本的位置,繼續執行下去

那電腦「記」在哪裡呢?

這些記錄會被放進一個地方,叫做 呼叫堆疊(Call Stack)

你可以把呼叫堆疊想成是電腦桌上的「便利貼堆疊區」,或像是一個盒子,每次你呼叫函式:

  • 就在盒子最上面放一張新便利貼(記住「我現在在哪裡,要去做什麼」)
  • 函式做完後,把最上面那張拿掉,再繼續做下面那張記的事

每次都這樣「上面那件事做完 → 拿掉 → 回到下面那件事」。

這種運作順序就叫做 後進先出(LIFO),也就是:

「最後放進去的任務,要先做完才能回去做前面的。」

來對照一下這段程式碼:

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

greet("Harry");
console.log("Bye");

這段執行過程會像這樣:

  1. 執行到 greet("Harry"),就像你「接到電話」:
    👉 先寫一張便利貼「我等等要回來執行 console.log("Bye")」放進呼叫堆疊裡。
  2. 然後去執行 greet 裡面的內容 → 印出 "Hello Harry"
  3. 執行完 greet,把剛剛的便利貼拿出來,回到 console.log("Bye") 繼續執行

小結

呼叫堆疊就像是「一疊任務便利貼」:

  • 每當你叫一個函式,電腦就寫一張便利貼:「我等等要回哪裡」
  • 把便利貼疊上去
  • 等函式做完,把最上面那張撕掉
  • 再看下一張繼續做

這整個機制,就是 JavaScript 幫你記住執行流程的方式,才不會在多層函式呼叫之間迷路。

呼叫堆疊是怎麼一層層堆起來的?

回到生活情境:你還記得你桌上的便利貼堆嗎?

想像你現在桌上放了一疊便利貼,每一張都代表「我現在暫停了什麼事情,要去做哪件新事」。

  • 這些便利貼是有順序的,最新的放在最上面,等一下就是先從最上面的開始做。
  • 每做完一件事,就會把那張便利貼撕掉,然後回到前一張記的那件事繼續做。

這整疊便利貼,就像是電腦裡的「呼叫堆疊(Call Stack)」。

JavaScript 程式在呼叫函式時,發生了什麼?

我們先看這段稍微複雜一點的程式碼:

function step3() {
  console.log("step3:最後一步");
}

function step2() {
  console.log("step2:做一半…");
  step3();
  console.log("step2:收尾");
}

function step1() {
  console.log("step1:開始做事");
  step2();
  console.log("step1:全部結束");
}

step1();

這段程式的執行順序表面看起來很直白,但讓我們進入電腦的腦袋,看看它的「便利貼堆(呼叫堆疊)」怎麼一層層變化。

一步步分析執行與堆疊變化

✅ 開始執行 step1()

  • 電腦一開始從 step1() 開始執行。
  • 它會在呼叫堆疊裡新增一張紀錄:「我正在執行 step1,這是從主程式來的」。

呼叫堆疊現在長這樣:

[ step1 來自主程式 ]

step1 執行第一行,印出「step1:開始做事」

  • 正常印出,沒呼叫其他函式,堆疊不變。

✅ 執行 step2() → 呼叫進入新的函式

  • 電腦遇到 step2(),這是一個新的任務。
  • 它會「暫停」現在的進度,在堆疊最上面再貼一張便利貼,記住:「我現在從 step1 要去做 step2」。

呼叫堆疊變化:

[ step2 來自 step1 ]
[ step1 來自主程式 ]

step2 執行第一行,印出「step2:做一半…」

  • 同樣只是印出內容,不影響堆疊。

✅ 執行 step3() → 再次呼叫新函式

  • 現在又遇到新的函式呼叫 step3()
  • 電腦再貼一張便利貼:「我現在從 step2 要去做 step3」。

呼叫堆疊變化:

[ step3 來自 step2 ]
[ step2 來自 step1 ]
[ step1 來自主程式 ]

step3 執行內容,印出「step3:最後一步」

  • 完成後,這個任務已經結束。
  • 電腦撕掉最上面那張便利貼,代表 step3() 做完了,要回到 step2() 的下一行繼續執行。

呼叫堆疊變化:

[ step2 來自 step1 ]
[ step1 來自主程式 ]

✅ 回到 step2(),執行第二行,印出「step2:收尾」

  • step2() 完整結束。
  • 電腦撕掉這張便利貼。

呼叫堆疊變化:

[ step1 來自主程式 ]

✅ 回到 step1(),執行最後一行,印出「step1:全部結束」

  • step1() 結束。
  • 呼叫堆疊清空,程式結束。

呼叫堆疊變化:

(清空)

📊 呼叫堆疊整體流程圖(由上到下,時間線)

呼叫堆疊狀態(由上到下)step1
呼叫堆疊狀態(由上到下)step2 → step1
呼叫堆疊狀態(由上到下)step3 → step2 → step1
呼叫堆疊狀態(由上到下)step2 → step1
呼叫堆疊狀態(由上到下)step1
呼叫堆疊狀態(由上到下)

呼叫堆疊會不會堆太多?會!這叫做「堆疊爆掉(Stack Overflow)」

🐔🥚 來看一個最容易理解的程式:雞生蛋,蛋又生雞…

function chicken() {
  return egg();
}

function egg() {
  return chicken();
}

chicken();

這段程式做了什麼?

  • chicken() 會呼叫 egg()
  • egg() 又會呼叫 chicken()
  • chicken() 又呼叫 egg()

它們永遠互相呼叫,完全沒有「停下來」的時機。

電腦發生了什麼事?

每一次呼叫一個函式,電腦都會把「我從哪裡來、等等要回哪裡」記在呼叫堆疊中。

我們來模擬這段程式剛開始執行時,呼叫堆疊的變化情況:

▶️ 執行 chicken()

呼叫堆疊變化:

[ chicken 來自主程式 ]

▶️ chicken() 裡呼叫 egg()

呼叫堆疊變化:

[ egg 來自 chicken ]
[ chicken 來自主程式 ]

▶️ egg() 裡呼叫 chicken()

呼叫堆疊變化:

[ chicken 來自 egg ]
[ egg 來自 chicken ]
[ chicken 來自主程式 ]

▶️ chicken() 裡又呼叫 egg()

呼叫堆疊變化:

[ egg 來自 chicken ]
[ chicken 來自 egg ]
[ egg 來自 chicken ]
[ chicken 來自主程式 ]

🌀 然後就會:

繼續這樣下去沒完沒了…

  • 每一次呼叫,堆疊都多一層
  • 完全沒有任何函式「執行完畢、離開堆疊」
  • 所以堆疊會一直成長、堆得越來越高

❌ 最後發生什麼事?

堆疊超過電腦限制,程式崩潰,錯誤訊息如下:

Uncaught RangeError: Maximum call stack size exceeded

這句話的意思就是:

❗「呼叫堆疊太滿,我沒辦法再記下任何函式了!」

圖解版

呼叫堆疊狀態[ chicken ]
呼叫堆疊狀態[ egg, chicken ]
呼叫堆疊狀態[ chicken, egg, chicken ]
呼叫堆疊狀態[ egg, chicken, egg, chicken ]
呼叫堆疊狀態(堆疊繼續增加直到爆掉)

總結:呼叫堆疊是你寫函式時的「幕後幫手」

不論是誰先誰後、誰呼叫誰,電腦都靠呼叫堆疊幫它記住流程。

你可以這樣記住呼叫堆疊的意義:

每次叫別人幫忙做事,我會先把自己現在做到哪裡寫在便條紙上,等別人做完後我再繼續我的事。這一疊便條紙就是呼叫堆疊。