本文 AI 產出,尚未審核

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

說明curryadd 可以接受任意分段方式的參數,提升彈性。


2️⃣ 預先綁定「配置」參數

假設有一個 request 函式需要 methodurlpayload 三個參數,我們可以先固定 methodPOST,產生專屬的 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,需在 applycall 中保留上下文。 使用 function curried(...args) { … } 而非箭頭函式,或在 apply(this, …) 時傳入正確的 this
過度柯里化 把所有函式都柯里化會導致呼叫階段過於繁瑣,降低可讀性。 僅在需要「預先綁定」或「函式組合」時使用,保持程式碼簡潔。
記憶體泄漏 每次呼叫柯里化函式會產生新的閉包,若大量產生未釋放會佔用記憶體。 適度使用 memoization(快取)或在不再需要時手動釋放參考。

最佳實踐

  1. 明確命名:針對已預先綁定的函式使用 xxxWithYpreXxx 之類的命名,讓呼叫者一眼看出意圖。
  2. 限制層數:若柯里化層數超過 3 層,考慮改用 partial application(部分應用)或 object configuration
  3. 結合 TypeScript:使用介面或類型別名描述柯里化函式的階段簽名,提升開發時的型別安全。
  4. 使用函式式函式庫:如 Ramdalodash/fp,它們已提供優化過的 currycomposepipe 等工具。

實際應用場景

  1. 表單驗證

    • 先設定驗證規則(如 requiredmaxLength),再把具體欄位值傳入,得到布林結果。
  2. Redux / Vuex 中的 Action Creators

    • 先固定 type,再傳入 payload,形成可重用的 Action 產生器。
  3. 國際化(i18n)文字函式

    • t = curry((locale, key, params) => …)t('zh-TW')('welcome')({ name: '小明' })
  4. API 客戶端

    • 為不同服務(user、order)預先綁定 baseURL、認證資訊,產生專屬的 request 函式。
  5. 單元測試的 Mock

    • 先建立「固定行為」的 mock 函式,再在測試中注入不同的參數,以驗證邏輯分支。

總結

函式柯里化是一項 提升程式可組合性與可讀性的利器。透過把多參數函式拆解成單參數的鏈式呼叫,我們能:

  • 預先綁定 常用參數,減少重複程式碼。
  • 建立資料管道,讓函式之間的流向更清晰。
  • 延遲執行,在所有資訊齊備時才觸發運算,對非同步流程特別有幫助。

在實務開發中,適度使用柯里化可以讓 API 設計更具彈性、測試更簡潔、程式碼更易維護。記得 避免過度柯里化保留正確的 this,並善用現有函式式函式庫,才能在日常開發中真正發揮它的威力。祝你在 JavaScript 的函式式旅程中玩得開心!