iT邦幫忙

2022 iThome 鐵人賽

DAY 20
1
Modern Web

致 JavaScript 開發者的 Functional Programming 新手指南系列 第 20

Day 20 :什麼是 Currying(2)?JavaScript Call Stack

  • 分享至 

  • xImage
  •  

在前一章節中,我們了解到 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

其實這就是所謂執行堆疊的概念,上方我們使用相對簡單的程式碼來示意,若是用圖來示意的話:

https://ithelp.ithome.com.tw/upload/images/20220922/20151147rmHauj6vQm.png

根據示意圖,我們可以將函式拆分為四個部分:

  1. 當我們呼叫 first 函式時,這個函式就進入的呼叫堆疊中。
  2. 由於我們在 first 函式中呼叫了 second 函式,此時呼叫堆疊中就有了兩個函式,最下面的是 first 函式, first 函式上又堆疊了 second 函式。
  3. second 函式結束,離開呼叫堆疊
  4. 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 技巧要聊聊,那就是「閉包」,那就讓我們下個章節見吧!

註解:

  1. 語法作用域(Lexical Scope):語法作用域指的是在 JavaScript 在執行時,可被參照引用的變數,例如: ES6 以後透過 letconst 關鍵字宣告變數,會因為區塊({}) 而分為全域作用域(Global Lexical Scope)與區域作用域(Regional Lexical Scope),當讀取到變數時,JavaScript 就會一層一層向外查找直到找不到此參照變數。

參考資料:

  1. MDN - Call stack
  2. MDN - The event loop
  3. MDN - Scope

上一篇
Day 19:什麼是 Currying(1)?瀏覽器運作簡介
下一篇
Day 21 :什麼是 Currying(3)?JavaScript 閉包
系列文
致 JavaScript 開發者的 Functional Programming 新手指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言