JavaScript – 函式柯里化(Currying)
簡介
在 JavaScript 中,函式是最基本的抽象單位,而 柯里化(Currying) 則是一種把「接受多個參數的函式」轉換成「接受單一參數的函式」的技巧。透過柯里化,我們可以把一個複雜的運算拆解成一連串簡單、可重用的步驟,讓程式碼更具可組合性、可讀性與可測試性。
在前端開發、函式式程式設計(Functional Programming)以及大型專案的模組化建構中,柯里化常被用來延遲執行、預先綁定參數,或是建立高階函式(higher‑order functions)。掌握柯里化,不僅能寫出更乾淨的程式,也能提升團隊協作時的程式一致性。
核心概念
什麼是柯里化?
柯里化(Currying)是指把一個 接受 n 個參數 的函式,轉換成 接受一個參數 並回傳另一個接受剩餘參數的函式,如此重複直到所有參數都被提供。最終得到的結果與原始函式直接呼叫的結果相同。
例子:
f(a, b, c)→f(a)(b)(c)
為什麼要使用柯里化?
| 優點 | 說明 |
|---|---|
| 參數預設 | 先傳入常用參數,產生「特化」版函式,之後只需要提供剩餘參數。 |
| 函式組合 | 可把小函式串接成更大的流程(pipeline)。 |
| 延遲執行 | 把計算推遲到所有參數都齊備時才執行,適合非同步或惰性求值。 |
| 可測試性 | 每一步只處理單一參數,單元測試更容易撰寫。 |
手寫柯里化函式
以下是一個通用的 curry 實作,支援任意參數長度:
/**
* 將任意函式轉換為柯里化版本
* @param {Function} fn 原始函式
* @returns {Function} 柯里化後的函式
*/
function curry(fn) {
// 取得原函式需要的參數數量
const arity = fn.length;
// 內部遞迴收集參數
function curried(...args) {
// 若已收集足夠參數,直接呼叫原函式
if (args.length >= arity) {
return fn.apply(this, args);
}
// 否則回傳一個繼續收集參數的函式
return (...more) => curried.apply(this, args.concat(more));
}
return curried;
}
以上程式碼中,
fn.length會返回函式宣告時的形參個數(即「arity」),我們利用閉包持續累積傳入的參數,直到滿足需求為止。
程式碼範例
1️⃣ 基本加法函式的柯里化
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log( curriedAdd(1)(2)(3) ); // 6
console.log( curriedAdd(1, 2)(3) ); // 6
console.log( curriedAdd(1)(2, 3) ); // 6
說明:
curry讓add可以接受任意分段方式的參數,提升彈性。
2️⃣ 預先綁定「配置」參數
假設有一個 request 函式需要 method、url、payload 三個參數,我們可以先固定 method 為 POST,產生專屬的 postRequest:
function request(method, url, payload) {
// 這裡僅示意,實際會使用 fetch / axios 等
console.log(`[${method}] ${url}`, payload);
}
const curriedRequest = curry(request);
const postRequest = curriedRequest('POST'); // 只需要提供 url、payload
postRequest('/api/users', { name: 'Alice' });
// 輸出: [POST] /api/users { name: 'Alice' }
實務意義:在大型專案中,常見多個 API 都使用相同的 HTTP 方法或通用 header,透過柯里化可以一次性預設,降低重複程式碼。
3️⃣ 組合資料過濾器(Pipeline)
// 基本過濾器
const filterByAge = min => person => person.age >= min;
const filterByCountry = country => person => person.country === country;
// 柯里化後的組合函式
function compose(...fns) {
return arg => fns.reduce((prev, fn) => fn(prev), arg);
}
// 建立客製化過濾器
const adultInTaiwan = compose(
filterByAge(18),
filterByCountry('Taiwan')
);
const users = [
{ name: '小明', age: 20, country: 'Taiwan' },
{ name: 'John', age: 25, country: 'USA' },
{ name: '阿美', age: 16, country: 'Taiwan' },
];
console.log( users.filter(adultInTaiwan) );
// 輸出: [{ name: '小明', age: 20, country: 'Taiwan' }]
說明:每個過濾器都是單參數的高階函式,透過
compose把它們串起來,形成可重用的資料管道。
4️⃣ 非同步柯里化(Promise)
function fetchJson(url, options) {
return fetch(url, options).then(res => res.json());
}
const curriedFetch = curry(fetchJson);
// 先綁定 API 根路徑
const apiFetch = curriedFetch('https://api.example.com');
// 再綁定共用的 header
const authorizedFetch = apiFetch({ headers: { Authorization: 'Bearer xxx' } });
authorizedFetch('/users')
.then(data => console.log(data))
.catch(err => console.error(err));
重點:柯里化同樣適用於返回 Promise 的非同步函式,讓 設定階段與 執行階段清晰分離。
5️⃣ 透過 ES6 Arrow Function 實作簡易柯里化
const multiply = a => b => a * b; // 兩層箭頭函式即為柯里化
const double = multiply(2);
console.log(double(5)); // 10
const triple = multiply(3);
console.log(triple(5)); // 15
適用情境:當函式參數固定且層數不多時,直接使用箭頭函式即可快速寫出柯里化版本,省去
curry的通用實作。
常見陷阱與最佳實踐
| 陷阱 | 可能的問題 | 解決方式 |
|---|---|---|
| 忘記返回函式 | 柯里化的每一步若未 return 下一層函式,會直接得到 undefined。 |
確保每個階段都 return 一個接受剩餘參數的函式。 |
| 參數數量不一致 | 使用 fn.length 時,若原函式使用 rest parameters (...args) 會得到 0,導致柯里化失效。 |
針對 ...args 的情況自行傳入期望的 arity,或改用 function.length 的手動設定。 |
this 綁定遺失 |
柯里化過程中若使用 this,需在 apply 或 call 中保留上下文。 |
使用 function curried(...args) { … } 而非箭頭函式,或在 apply(this, …) 時傳入正確的 this。 |
| 過度柯里化 | 把所有函式都柯里化會導致呼叫階段過於繁瑣,降低可讀性。 | 僅在需要「預先綁定」或「函式組合」時使用,保持程式碼簡潔。 |
| 記憶體泄漏 | 每次呼叫柯里化函式會產生新的閉包,若大量產生未釋放會佔用記憶體。 | 適度使用 memoization(快取)或在不再需要時手動釋放參考。 |
最佳實踐
- 明確命名:針對已預先綁定的函式使用
xxxWithY或preXxx之類的命名,讓呼叫者一眼看出意圖。 - 限制層數:若柯里化層數超過 3 層,考慮改用 partial application(部分應用)或 object configuration。
- 結合 TypeScript:使用介面或類型別名描述柯里化函式的階段簽名,提升開發時的型別安全。
- 使用函式式函式庫:如 Ramda、lodash/fp,它們已提供優化過的
curry、compose、pipe等工具。
實際應用場景
表單驗證
- 先設定驗證規則(如
required、maxLength),再把具體欄位值傳入,得到布林結果。
- 先設定驗證規則(如
Redux / Vuex 中的 Action Creators
- 先固定
type,再傳入payload,形成可重用的 Action 產生器。
- 先固定
國際化(i18n)文字函式
t = curry((locale, key, params) => …)→t('zh-TW')('welcome')({ name: '小明' })。
API 客戶端
- 為不同服務(user、order)預先綁定 baseURL、認證資訊,產生專屬的 request 函式。
單元測試的 Mock
- 先建立「固定行為」的 mock 函式,再在測試中注入不同的參數,以驗證邏輯分支。
總結
函式柯里化是一項 提升程式可組合性與可讀性的利器。透過把多參數函式拆解成單參數的鏈式呼叫,我們能:
- 預先綁定 常用參數,減少重複程式碼。
- 建立資料管道,讓函式之間的流向更清晰。
- 延遲執行,在所有資訊齊備時才觸發運算,對非同步流程特別有幫助。
在實務開發中,適度使用柯里化可以讓 API 設計更具彈性、測試更簡潔、程式碼更易維護。記得 避免過度柯里化、保留正確的 this,並善用現有函式式函式庫,才能在日常開發中真正發揮它的威力。祝你在 JavaScript 的函式式旅程中玩得開心!