JavaScript - 呼叫堆疊

你好,未來的 JavaScript 巫師們!今天,我們將深入探讨 JavaScript 最基礎的概念之一:呼叫堆疊(Call Stack)。別擔心如果你之前從未聽過它——到了這個教學的結尾,你將會成為呼叫堆疊的專家!所以,拿起你喜歡的飲料,放鬆心情,讓我們一起踏上這個令人興奮的旅程。

JavaScript - Call Stack

呼叫堆疊是什麼?

在我們進入細節之前,讓我們先用一個簡單的比喻來說明。想像你正在閱讀一本選擇自己的冒險書。當你閱讀時,你會在每個決策點放一個書籤。當你走完一條路徑時,你會回到上一個書籤,嘗試另一條路徑。JavaScript 的呼叫堆疊工作原理與此相似——它記錄了程序在執行完函數後應該返回的位置。

從技術角度來說,呼叫堆疊是一種數據結構,它使用後進先出(Last In, First Out, LIFO)的原則來暫時存儲和管理 JavaScript 中的函數調用(呼叫)。

JavaScript 呼叫堆疊如何工作?

現在,讓我們看看呼叫堆疊在 JavaScript 中是如何實際工作的。我們將從一個簡單的例子開始,然後逐漸增加複雜性。

範例 1:一個簡單的函數調用

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

greet("Alice");

當這段代碼運行時,在呼叫堆疊中會發生以下情況:

  1. greet 函數被推入堆疊。
  2. 函數執行,將問候語記錄到控制台。
  3. 函數完成並從堆疊中彈出。

相當直觀,對吧?現在,讓我們看一個稍微複雜一點的例子。

範例 2:嵌套函數調用

function multiply(a, b) {
return a * b;
}

function square(n) {
return multiply(n, n);
}

function printSquare(n) {
var squared = square(n);
console.log(n + " squared is " + squared);
}

printSquare(4);

當我們運行 printSquare(4) 時,呼叫堆疊會按以下方式操作:

  1. printSquare(4) 被推入堆疊。
  2. printSquare 內部,square(4) 被調用並推入堆疊。
  3. square 內部,multiply(4, 4) 被調用並推入堆疊。
  4. multiply 完成並從堆疊中彈出。
  5. square 完成並從堆疊中彈出。
  6. printSquare 記錄結果並完成,然後從堆疊中彈出。

你能看到隨著函數的調用和完成,堆疊是如何增長和縮小的嗎?這就像一個樂高積木塔被建造起來又拆掉!

範例 3:遞歸函數

遞歸函數是展示呼叫堆疊如何增長的絕佳方式。讓我們看一個經典的例子:計算階乘。

function factorial(n) {
if (n === 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}

console.log(factorial(5));

當我們調用 factorial(5) 時,呼叫堆疊將如下所示:

  1. factorial(5) 被推入
  2. factorial(4) 被推入
  3. factorial(3) 被推入
  4. factorial(2) 被推入
  5. factorial(1) 被推入
  6. factorial(1) 返回 1 並從堆疊中彈出
  7. factorial(2) 計算 2 * 1,返回 2,並從堆疊中彈出
  8. factorial(3) 計算 3 * 2,返回 6,並從堆疊中彈出
  9. factorial(4) 計算 4 * 6,返回 24,並從堆疊中彈出
  10. factorial(5) 計算 5 * 24,返回 120,並從堆疊中彈出

哇!這有點複雜,對吧?但這正是 JavaScript 如何追蹤所有那些嵌套函數調用的方式。

JavaScript 呼叫堆疊溢出

現在,我們理解了呼叫堆疊是如何工作的,讓我們來討論當事情出错時會發生什麼。你有没有聽過“堆疊溢出”(stack overflow)這個術語?這不僅是一個絕望的程序員的網站(儘管它也是那樣)——這是實際上可能發生在你的代碼中的錯誤。

堆疊溢出發生在當有太多的函數調用,且呼叫堆疊超出了它的大小限制。最常見的原因?無窮遞歸!

範例 4:堆疊溢出

function causeStackOverflow() {
causeStackOverflow();
}

causeStackOverflow();

如果你運行這段代碼,你會得到一個像“Maximum call stack size exceeded”這樣的錯誤信息。這就像嘗試建造一個到月球的樂高積木塔——最終,你會用完積木(或者在本例中是記憶體)!

為了避免堆疊溢出,總是確保你的遞歸函數有一個正確的終止條件來終止遞歸。

呼叫堆疊方法

JavaScript 沒有提供直接操作呼叫堆疊的方法,但有一些相關的函數對於調試和理解呼叫堆疊非常有用:

方法 描述
console.trace() 將堆疊跟踪輸出到控制台
Error.stack 一個非標準屬性,返回堆疊跟踪

這裡有一個使用 console.trace() 的快速範例:

function func1() {
func2();
}

function func2() {
func3();
}

function func3() {
console.trace();
}

func1();

這將輸出一個堆疊跟踪,顯示調用序列:func3 -> func2 -> func1

結論

好了,各位!我們已經穿越了 JavaScript 呼叫堆疊的迷人世界。從簡單的函數調用到複雜的遞歸,你現在理解了 JavaScript 如何在代碼中追蹤位置。

記住,呼叫堆疊就像一個有用的助手,總是記住你在 JavaScript 故事書中的位置。但就像任何好助手一樣,它也有自己的限制——所以對它好一點,避免那些討厭的堆疊溢出!

在你繼續你的 JavaScript 冒險時,請記住呼叫堆疊。理解它不僅會幫助你寫出更好的代碼,也會讓調試變得更加容易。快樂編程,願你的堆疊永遠完美平衡!

Credits: Image by storyset