本文 AI 產出,尚未審核
JavaScript 高階函式(Higher‑order Functions)教學
簡介
在 JavaScript 的日常開發中,我們常會看到 「函式」 被當作第一等公民(first‑class citizens)來使用。這意味著函式本身可以像變數一樣被傳遞、儲存、回傳,甚至作為其他函式的參數或回傳值。能夠靈活運用這種特性,我們就能寫出 更簡潔、可重用且具備抽象能力 的程式碼。
高階函式(Higher‑order Functions,簡稱 HOF)正是利用這個概念的核心工具。它們接受函式作為參數,或回傳一個新的函式,讓我們可以把 「行為」 抽象成 「資料」,進而在程式中建立通用的操作流程、減少重複程式碼、提升可測試性。無論是陣列操作、事件處理、或是函式式程式設計(functional programming)風格的實作,高階函式都是不可或缺的基礎。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,一路帶你深入了解高階函式的威力,並提供實務上常見的應用情境,讓你在寫 JavaScript 時能更得心應手。
核心概念
什麼是高階函式?
高階函式:接受函式作為參數,或回傳函式的函式。
簡單來說,只要滿足以下任一條件,即屬於高階函式:
- 接收另一個函式作為參數(例如
Array.prototype.map)。 - 回傳一個函式(例如函式工廠
createAdder)。 - 同時具備上述兩者(例如
compose)。
為什麼要使用高階函式?
- 抽象化重複邏輯:把「遍歷陣列」或「錯誤處理」等共通流程抽離成高階函式。
- 提升可讀性:使用語意化的高階函式(如
filter、reduce)可以讓程式碼更貼近自然語言。 - 函式組合:透過組合(compose)或柯里化(currying)等技巧,建立更小、更專注的函式,再組合成更複雜的行為。
下面分別用 實作範例 來說明這些概念。
程式碼範例
1️⃣ Array.prototype.map:最常見的高階函式
// 將每個元素乘以 2
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8]
map接受一個回呼函式num => num * 2,對陣列的每個元素執行,回傳一個新陣列。- 這樣的寫法比起傳統的
for迴圈更具可讀性,也避免手動管理索引。
2️⃣ 自訂高階函式:filterBy
/**
* 依照條件過濾陣列
* @param {Array} arr 要過濾的陣列
* @param {Function} predicate 判斷條件,回傳 true 表示保留
* @returns {Array} 過濾後的結果
*/
function filterBy(arr, predicate) {
const result = [];
for (const item of arr) {
if (predicate(item)) result.push(item);
}
return result;
}
// 範例:過濾出偶數
const nums = [1, 2, 3, 4, 5, 6];
const evens = filterBy(nums, n => n % 2 === 0);
console.log(evens); // [2, 4, 6]
filterBy接受一個陣列與一個判斷函式(predicate),把「過濾」的邏輯抽象化。- 之後若要改變過濾條件,只需要傳入不同的 predicate,而不必改動
filterBy本身。
3️⃣ 回傳函式的高階函式:函式工廠 createAdder
/**
* 建立一個「加法」函式
* @param {number} x 基礎值
* @returns {Function} 接收另一個數字並回傳 x + y
*/
function createAdder(x) {
return function (y) {
return x + y;
};
}
const addFive = createAdder(5);
console.log(addFive(3)); // 8
console.log(addFive(10)); // 15
createAdder回傳一個新函式,這個新函式「記住」了外層的x(閉包)。- 這種模式在 設定預設參數、建立可重用的工具函式 時非常有用。
4️⃣ 函式組合(Compose):把多個函式串起來
/**
* 從右到左組合多個函式
* @param {...Function} fns 要組合的函式
* @returns {Function} 組合後的函式
*/
function compose(...fns) {
return function (arg) {
return fns.reduceRight((prev, fn) => fn(prev), arg);
};
}
// 小工具函式
const double = n => n * 2;
const square = n => n ** 2;
const decrement = n => n - 1;
// 組合成新函式:先遞減,再平方,最後再乘以 2
const complexOperation = compose(double, square, decrement);
console.log(complexOperation(5)); // ((5 - 1)²) * 2 = 32
compose接受多個函式,回傳一個新函式,執行順序是從右到左(decrement → square → double)。- 這種寫法讓 資料流 更清晰,且易於單元測試每個小函式。
5️⃣ Array.prototype.reduce:累積與抽象化
const purchases = [
{ item: '筆記本', price: 1200 },
{ item: '滑鼠', price: 350 },
{ item: '鍵盤', price: 850 },
];
// 計算總金額
const total = purchases.reduce((sum, p) => sum + p.price, 0);
console.log(total); // 2400
reduce接受一個累加器函式,將陣列元素「累積」成單一結果。- 透過
reduce,我們可以把 統計、分組、扁平化 等複雜操作抽象成簡潔的函式呼叫。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 不小心改變原始資料 | 某些高階函式(如 map、filter)會回傳新陣列,但若在回呼函式內直接修改原始元素,仍會產生副作用。 |
避免在回呼函式中改變傳入的參數,若需要變更,先 clone 再操作。 |
| 過度嵌套(callback hell) | 多層高階函式或非同步回呼容易造成程式碼難以閱讀。 | 使用 函式組合、柯里化 或 async/await 把流程平鋪。 |
忘記傳遞 this |
某些情況下,回呼函式需要正確的 this(例如陣列方法的第二參數)。 |
使用 箭頭函式(自動綁定 this)或 Function.prototype.bind。 |
| 不恰當的參數順序 | compose、pipe 等組合函式的參數順序若搞錯,會導致結果相反。 |
先在小範例驗證順序,再寫正式程式。 |
| 過度抽象 | 把所有邏輯都包成高階函式,會讓新手難以追蹤實際執行流程。 | 保持適度抽象:只把真正可重用且通用的部分抽成高階函式。 |
最佳實踐
- 純函式(pure function)優先:不改變外部狀態、只靠輸入產生輸出。
- 命名語意化:高階函式的名稱應該能描述其行為,如
filterBy,groupBy。 - 小函式:每個函式只做一件事,組合時再形成複雜功能。
- 單元測試:高階函式本身與其接受的回呼函式都應有測試,確保組合後的行為正確。
- 使用內建 API:
map、filter、reduce、some、every等已優化且易讀,除非有特殊需求,盡量不要自行實作。
實際應用場景
1️⃣ 表單驗證管線
// 各種驗證規則(回呼函式)
const isRequired = value => value !== '' || '此欄位必填';
const isEmail = value => /\S+@\S+\.\S+/.test(value) || '必須是有效的 Email';
const minLength = len => value => value.length >= len || `至少需要 ${len} 個字元`;
// 建立驗證管線的高階函式
function createValidator(...rules) {
return function (value) {
for (const rule of rules) {
const result = rule(value);
if (result !== true) return result; // 失敗立即回傳錯誤訊息
}
return true;
};
}
// 使用範例
const validateEmail = createValidator(isRequired, isEmail);
const validatePassword = createValidator(isRequired, minLength(8));
console.log(validateEmail('')); // "此欄位必填"
console.log(validateEmail('test@example.com')); // true
console.log(validatePassword('12345')); // "至少需要 8 個字元"
- 高階函式
createValidator把多個驗證規則組合成一個「驗證管線」。 - 新增或調整規則只需要改變
rules,不必改動核心驗證邏輯。
2️⃣ API 請求攔截與重試
/**
* 包裝 fetch,加入自動重試機制
* @param {Function} fetchFn 原始 fetch 函式
* @param {number} retries 最大重試次數
* @returns {Function}
*/
function withRetry(fetchFn, retries = 3) {
return async function (url, options) {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await fetchFn(url, options);
if (!response.ok) throw new Error('HTTP error');
return response;
} catch (err) {
if (attempt === retries) throw err; // 最後一次仍失敗,拋出錯誤
console.warn(`第 ${attempt + 1} 次重試...`);
}
}
};
}
// 使用
const fetchWithRetry = withRetry(fetch, 2);
fetchWithRetry('https://api.example.com/data')
.then(res => res.json())
.then(console.log)
.catch(console.error);
withRetry接受原始的fetch,回傳一個具備自動重試功能的新函式。- 讓所有 API 呼叫只要換成
fetchWithRetry,即可統一處理錯誤與重試。
3️⃣ UI 事件去抖(debounce)與節流(throttle)
// debounce 高階函式
function debounce(fn, wait) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}
// throttle 高階函式
function throttle(fn, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// 範例:搜尋框即時建議(使用 debounce)
const input = document.getElementById('search');
input.addEventListener('input', debounce(event => {
console.log('搜尋關鍵字:', event.target.value);
// 這裡可以發送 API 請求
}, 300));
debounce與throttle都是 函式工廠,它們把「頻繁觸發」的行為包裝成更安全的版本。- 在前端開發中,這兩個高階函式是 提升效能與使用者體驗 的常見手段。
總結
高階函式是 JavaScript 讓函式具備 「可傳遞、可組合、可產生」 能力的關鍵特性。透過接受函式作為參數或回傳函式,我們可以:
- 抽象出 共通流程(如
filterBy、withRetry); - 組合 多個小函式形成更複雜的行為(
compose、pipe); - 實現 函式式程式設計 的核心概念,如 純函式、柯里化、不可變性;
- 在 表單驗證、API 請求、UI 事件處理 等實務場景中,寫出更簡潔、可維護且易測試的程式碼。
在日常開發時,請記得:
- 先寫純函式,再把它們包成高階函式;
- 適度抽象,避免過度抽象造成閱讀困難;
- 善用內建高階函式(
map、filter、reduce)與 自訂工具函式(compose、debounce); - 測試 每一層函式,確保組合後的行為仍正確。
掌握了高階函式的概念與實作,你的 JavaScript 程式碼將會變得更具彈性、可重用性與可讀性。祝你在未來的開發旅程中,玩得開心、寫得順手! 🚀