本文為 JS 底層運作邏輯系列文,第二篇
- 初學者指南:深入了解 JavaScript 中的 Event Loop(事件循環)
- 初學者指南:深入了解 JavaScript 的 Call Stack(呼叫堆疊) 👈 所在位置
- 初學者指南:深入了解 JavaScript 的執行環境(Execution Context)
- 初學者指南:深入了解 JavaScript 的建立期與執行期
- 初學者指南:深入了解 JavaScript 中函式與變數的建立期與執行期差異
當你在寫程式時,會把一些重複的動作寫成函式,然後在需要的時候叫它幫你做事。
舉例來說:
function greet(name) {
console.log("Hello " + name);
}
greet("Harry");
console.log("Bye");這段程式中,電腦的行為就像這樣:
- 一開始,電腦會先掃過整個程式碼,先記住有哪些函式(像是
greet)的定義,這叫做建立函式記憶(在技術上這步驟屬於「建立階段」或「Hoisting」)。 - 接著,程式從
greet("Harry")這一行開始執行,這是你叫電腦去執行greet裡的程式。 - 為了執行
greet,電腦會記下現在執行的位置,然後跳去greet函式裡,執行console.log("Hello Harry")。 - 等
greet做完後,電腦會回到剛剛中斷的地方的下一行,也就是console.log("Bye")。
為了讓這一切能正確進行,電腦需要一個東西來記住「我剛剛執行到哪裡、等一下要接著做什麼」──這個東西就是呼叫堆疊。
這篇文章會用清楚的步驟幫你理解:
- 為什麼需要呼叫堆疊?
- 它在做什麼?
- 如果出錯(例如無限呼叫)會發生什麼事?
什麼是「呼叫堆疊」?
先從生活情境說起:你是個忙碌的人
想像你今天在家裡做很多事:
你正在 廚房煮泡麵,這時候:
- 📞 電話響了,你怕泡麵煮太久,於是先關火,然後拿一張便條紙寫下:
✅「煮泡麵到第2分鐘,水還沒滾完」
- 🏃♂️ 接起電話,講到一半又有人按門鈴,你怕等一下忘了電話內容,又再拿一張便條紙寫下:
✅「電話講到媽媽說她晚上要來吃飯」
- 🛎 接著你去開門,是外送員,簽收完餐點之後,你回來看到便條紙,知道你要回去接著講電話。
- 電話講完之後,看回第一張便條紙,知道要回廚房繼續煮泡麵。
你有沒有發現?
每次「暫停一件事去處理另一件事」,你都會在桌上放一張便條紙記錄目前的進度,等處理完,就一張一張往回收拾,回到剛剛中斷的地方繼續。
呼叫堆疊的概念就是這樣來的
在 JavaScript 中,當你呼叫一個函式,電腦就會做一樣的事:
- 在呼叫函式之前,把目前程式的位置記起來
- 然後「暫停原本的工作」,轉去執行你呼叫的那個函式
- 等那個函式執行完,再「根據剛剛的記錄」回來原本的位置,繼續執行下去
那電腦「記」在哪裡呢?
這些記錄會被放進一個地方,叫做 呼叫堆疊(Call Stack)。
你可以把呼叫堆疊想成是電腦桌上的「便利貼堆疊區」,或像是一個盒子,每次你呼叫函式:
- 就在盒子最上面放一張新便利貼(記住「我現在在哪裡,要去做什麼」)
- 函式做完後,把最上面那張拿掉,再繼續做下面那張記的事
每次都這樣「上面那件事做完 → 拿掉 → 回到下面那件事」。
這種運作順序就叫做 後進先出(LIFO),也就是:
「最後放進去的任務,要先做完才能回去做前面的。」
來對照一下這段程式碼:
function greet(name) {
console.log("Hello " + name);
}
greet("Harry");
console.log("Bye");這段執行過程會像這樣:
- 執行到
greet("Harry"),就像你「接到電話」:
👉 先寫一張便利貼「我等等要回來執行console.log("Bye")」放進呼叫堆疊裡。 - 然後去執行
greet裡面的內容 → 印出"Hello Harry" - 執行完
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 | step1 |
| step1 呼叫 step2 | step2 → step1 |
| step2 呼叫 step3 | step3 → 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() | [ chicken ] |
chicken → egg | [ egg, chicken ] |
egg → chicken | [ chicken, egg, chicken ] |
chicken → egg | [ egg, chicken, egg, chicken ] |
| …無限持續中 | (堆疊繼續增加直到爆掉) |
總結:呼叫堆疊是你寫函式時的「幕後幫手」
不論是誰先誰後、誰呼叫誰,電腦都靠呼叫堆疊幫它記住流程。
你可以這樣記住呼叫堆疊的意義:
每次叫別人幫忙做事,我會先把自己現在做到哪裡寫在便條紙上,等別人做完後我再繼續我的事。這一疊便條紙就是呼叫堆疊。