在前一章節中,我們了解到 JavaScript 從我們撰寫出來,到可以被執行這個過程中,其實在背後會發生很多事,其中就包含能優化 JavaScript 單執行緒的 Event Loop 機制。
在 Event Loop 機制中,同步執行的函式會被丟進執行堆疊中,依據後進先出的順序來執行函式。
而說到執行堆疊時,其實也是指 JavaScript 函式被呼叫時,會執行的類似機制:呼叫堆疊(Call Stack)。
呼叫堆疊可以說是要更深入了解 JavaScript 函式運作的必備基礎知識,理解呼叫堆疊的機制,可以更幫助我們在撰寫函式時的效能,在這個章節中,我們就要在 Event Loop 中執行堆疊機制的基礎上,更深入的討論呼叫堆疊。
根據 MDN 文件說明,呼叫堆疊是一種呼叫多重函式時會出現的機制,例如我們在函式中再次呼叫函式,在第二個函式中,再次呼叫其他函式的狀況。
這樣聽起來是不是很不好理解?讓我們來看看程式碼範例:
function second () {
console.log('second Function starts');
console.log('second Function ends');
};
function first() {
console.log('first Function starts')
second();
console.log('first Function ends')
};
first();
根據上方的範例,大家覺得文字印出的順序是什麼呢?相信對有寫過 JavaScript 的開發者們,這完全是一片小蛋糕!
沒有錯,上方的範例文字會依照下方的順序印出來:
// first Function starts
// second Function starts
// second Function ends
// first Function ends
其實這就是所謂執行堆疊的概念,上方我們使用相對簡單的程式碼來示意,若是用圖來示意的話:
根據示意圖,我們可以將函式拆分為四個部分:
first
函式時,這個函式就進入的呼叫堆疊中。first
函式中呼叫了 second
函式,此時呼叫堆疊中就有了兩個函式,最下面的是 first
函式, first
函式上又堆疊了 second
函式。second
函式結束,離開呼叫堆疊first
函式結束,離開呼叫堆疊如此就是一個簡單的呼叫堆疊的運作模式了,但聰明的各位可能會有另一個疑惑:「參數呢?如果我在函式中帶入參數,在呼叫堆疊中會怎麼處理參數呢?」
根據 MDN 有關 Event Loop 概念的說明,其中提到:「當我們呼叫函式時,會在呼叫堆疊中根據函式的參數、區域變數,產生參考值。」
也就是說,只要這個執行堆疊沒有結束,我們就可以取到在這個堆疊中所有存在的參數與變數,這樣說明可能會有點抽象,讓我們來看看下面的範例:
function second (num) {
console.log(`print second number: ${num + 1}`);
};
function first(num) {
console.log(`print first number: ${num} `)
second(num);
};
first(1);
// print first number: 1
// print second number: 2
這樣的程式碼看起來很直覺對不對?就是透過一個參數接著向下傳遞參數的概念,那如果我們把程式碼改成:
function first(num) {
console.log(`print first number: ${num} `)
return function () {
console.log(`print second number: ${num + 1}`);
};
};
first(1)();
// output = ?
咦!你會發現我們獲得的結果竟然跟上一個範例一樣耶!
在這一個範例中,我們在 first
函式中,回傳了原本的 second
函式,由於現在了解執行堆疊的概念,所以但我們不需要在回傳函式中再次傳入參數,即可透過上一層的函式取得參數。
那你說我可以這麼撰寫嗎?
function first(num) {
console.log(`print first number: ${num} `)
second();
};
function second () {
console.log(`print second number: ${num + 1}`);
};
first(1);
當然不行!因為在執行到 second 函式時,因為找不到外層有任何有關 num 的變數,所以會產生 ReferenceError: num is not defined
的錯誤。
由此可知,雖然在堆疊中我們可以取得上一層函式的變數與參數的參考值,但前提是程式碼的撰寫有符合語法作用域的規範(可以向上查找到相對應的參考值,可參見註一)。
那另外一個問題來了,函式中的區域變數與參數的記憶體是如何運作的呢?
執行堆疊中的區域變數與參數的記憶體有個非常重要的特性,他們的生命週期與執行堆疊中的函式一致的!
當函式進入到執行堆疊中,該函式的區域變數與參數會一直保留在執行堆疊中,當函式離開執行堆疊後,該函式的區域變數與參數也會跟著釋放掉,應用這個特性可以有效優化函式的效能,避免不必要的全域變數而佔了 JavaScript 本身的記憶體空間。
在這個章節中,我們理解了所謂執行堆疊是 Event Loop 中的一個機制,更是貫穿「JavaScript 函式」運作的基礎概念,由於函式是 FP 設計模式中最重要的工具,我們必須把函式的特性搞懂,才可以用更有效率、乾淨的手段來撰寫程式碼。
在了解「科里化」之前,我們還有一個很重要的 JavaScript 技巧要聊聊,那就是「閉包」,那就讓我們下個章節見吧!
let
、const
關鍵字宣告變數,會因為區塊({}
) 而分為全域作用域(Global Lexical Scope)與區域作用域(Regional Lexical Scope),當讀取到變數時,JavaScript 就會一層一層向外查找直到找不到此參照變數。