什麼是高階函式 (Higher order function)?使用高階函式有什麼好處?
2025年3月7日
相信有些人在一些面試考古題中,看過「什麼是高階函式? 為什麼要用高階函式?」等相關問題。在這篇文章中,我們將來探討這個問題。
高階函式不只是面試會被問,在日常工作上,也很常出現,推薦還不熟的讀者要花些時間理解。
高階函式 (Higher order function) 是什麼?
高階函式 (Higher order function) 是指當一個函式可以接受另一個函式作為參數、或者返回一個函式作為結果的函式。
這是一種函式語言程式設計 (functional programming) 的概念,不只 JavaScript,許多程式語言都有支援高階函式,例如:Python、Swift 等。
它的重要性在於,可以幫助提高程式碼的模塊化和可維護性,因此很常應用到不同情境中,底下是一些例子:
- 回調函式 (callback function) 包裝:可以將回調函式作為參數傳遞給另一個函式,以實現自定義的行為。
- 抽象操作:可以將高階函式作為封裝操作的一層抽象,例如實現過濾、轉換等。
- 函式組合:可以使用高階函式組合多個函式,以創建新的函式。
如果要直接背誦這題的回答,可以回說「高階函式 (Higher order function) 是指可以接受另一個函式作為參數、或者會回傳一個函式作為結果的函式」。
而當要進一步回說「為什麼要用高階函式」時,則可以回說「透過高階函式,程式碼的可讀性可以提升,且能夠減少不必要的重複,同時也能夠容易除錯」。
假如你過去對高階函式沒有太多概念,推薦不要直接背這兩個回答。以下我們會透過多數人很熟悉的高階函式 map
、 reduce
以及 filter
作為範例切入,讓讀者們理解高階函式在做什麼,同時感受到高階函式的好處。
內建的高階函式 map
在多數的程式語言中,都有 map
這個內建的高階函式。map
函式在做的事情很簡單,一般來說會有兩個參數,一個參數是函式,一個是某個可迭代的東西 (例如陣列),然後 map
會迭代過這個可迭代的東西,然後在每次迭代時把值套到函式當中。
這樣講起來可能有點抽象,讓我們實際來看例子。以 Python 的內建 map
函式,官方文件的定義是 return an iterator that applies function to every item of iterable, yielding the results. 基本上與上面對 map
的定義是相同的。
讓我們實際來看可以怎麼在 Python 中用 map
l = [-1, -2, -3]
print(list(map(abs,l))) # [1, 2, 3]
以上面的例子來說,核心重點是 map(abs,l)
,這邊 map
接收的第一個引數是 abs
是 Python 內建把數字轉成絕對值得函式,第二個則是 [-1, -2, -3]
這個可迭代的列表,所以會把 -1
、-2
、-3
分別套到 abs
中,獲得絕對值。因此當我們把 map
的結果再透過 list
轉成列表後引出來,就會獲得 [1, 2, 3]
。
再來看看多數前後端工程師都熟悉的 JavaScript,雖然 map
在 JavaScript 當中是 Array
物件的方法,但其概念仍屬於高階函式的範疇。
在 MDN 上的範例如下:
const array1 = [1, 4, 9, 16];
// Pass a function to map
const map1 = array1.map((x) => x * 2);
console.log(map1);
// Expected output: Array [2, 8, 18, 32]
可以看到,如上面對高階函式的定義中談到高階函式是「接受另一個函式作為參數」的函式,這邊的 map
接收了 (x) => x * 2
這個函式,因此可被歸類在高階函式當中。而也如前面對 map
的定義,在 JavaScript 的 map
會迭代過陣列,然後把傳入的函式套到陣列中的每個元素。因此,這邊原本的 array1
是 [1, 4, 9, 16]
,在每個元素都套上 (x) => x * 2
,就會獲得 [2, 8, 18, 32]
如何實作 map?
在了解完 map
這個高階函式後,假如我們想要實作一個最陽春版本的 map
,可以怎麼做呢?
讓我們看到下面這個最陽出的版本 (僅處理陣列,不考量其他可迭代物)。這個 map
會接收一個陣列以及函式,而在 map
裡面會宣告一個 result
陣列,然後迭代過陣列時,把每個元素 item
丟到 callback
函式中,在把取得的結果放到 result
,最後回傳 result
即可。
// map 會接受一個陣列,以及一個回呼函式
function map(arr, callback) {
// 先宣告一個最後要回傳的陣列
const result = [];
// 用 for 迴圈迭代過陣列
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
// 把每個陣列元素丟到 callback 函式,並把結果放到 result 當中
result.push(callback(item));
}
return result;
}
重新理解高階函式的好處
在了解完 map
在做什麼,以及簡單實作一個陽春版的 map
後,讓我們重訪「為什麼要用高階函式? 」這個問題。還記得,上面提到高階函式的好處在於「程式碼的可讀性可以提升,且能夠減少不必要的重複,同時也能夠容易除錯」。
以 map
的例子來說,當今天有「要迭代過陣列,然後用同樣邏輯轉換陣列中的元素」的需求時,就可以直接用 map
,不需用每次都重新寫類似的程式碼。與此同時,可以把核心焦點放在最重要的邏輯上。當今天要維護的時候,也只需要去改最核心的邏輯即可,這樣維護起來會簡單很多。
上面談到的 map
是屬於「接收函式」的類型,還記得前面談到,高階函式的定義當中,除了「接收函式」之外,第二個定義是「當某個函式回傳另一個函式」,這也會被稱為高階函式。
讓我們來看一個具體的例子,是 LeetCode 2623 題,也是很常會在面試中出現的考題 memoize
函式的實作。這個函式會把過去已經運算過的輸出存起來,如果之後有同樣的輸入,未來就不用再運算一次,而是直接從快取拿之前算過的。
具體來說,memoize 會接收一個函式,然後回傳一個帶有記憶化 (快取) 的函式。對輸出的函式來說,如果某個輸入已經被運算過,就不會重複運算,直接從快取回傳即可。
// 舉例來說,有個 sum 函式,會回傳兩個參數的相加
const sum = (a, b) => a + b;
// 今天如果被 memoize 後獲得 memoizedSum,會有以下的作用
const memoizedSum = memoize(sum);
memoizedSum(2, 2); // 4 這是經過運算的
memoizedSum(2, 2); // 4 這是直接從快取拿的,不用再次運算
可以看到,透過 memoize
這個高階函式,不管什麼樣的函式,如果想要記憶化,都不需用重新寫一遍,而是可以直接用 memoize
來包即可,讓可讀性提高,同時減少不必要的重複。
備註:想了解這個 memoize
如何實作,可以參考 ExplainThis 寫過的解答
閱讀更多
上面透過 map
來討論高階函式是什麼、為什麼要用。關於高階函式的更多內容,包含常見的 reduce
以及 filter
,還有實務上運用高階函式的案例,都在 E+ 成長計畫的主題文有更深入的解說。
除了高階函式這個常見的面試題,E+ 成長計畫上個月也上架了《軟體工程師求職全攻略》 這堂長 8 小時 57 分鐘的課程,收錄了過去我們協助讀者們求職的精華重點,從如何寫履歷,到如何準備行為、技術面試,以及在過程中遇到挫折時如何調整心態。
有興趣的讀者歡迎加入 E+,在求職過程一起加油~