人人都会AI编程

9.6 函数柯里化、偏函数与函数组合

更新时间:2026-07-11

函数是 JavaScript 中的一等公民,这意味着函数可以作为参数传递,也可以作为返回值返回。第 9.3 节介绍的高阶函数为函数式编程风格打开了大门,而本节要讨论的三种模式——柯里化、偏函数和函数组合——正是建立在“函数即值”这一基石上的高级技法。它们让代码更模块化、更可复用,在实际项目中经常出现在数据管道、事件处理、表单校验等场景中。

9.6.1 函数柯里化(Currying)

定义
柯里化是将一个接受多个参数的函数,转换为一系列每次只接受一个参数的函数的过程。换句话说,原函数 f(a, b, c) 经过柯里化后,变为 f(a)(b)(c) 的调用形式。每个步骤返回一个新函数,等待下一个参数。

简单示例

// 普通多参数函数
function add(a, b, c) {
  return a + b + c;
}
console.log(add(1, 2, 3)); // 6

// 手动柯里化:先接受a,返回接受b的函数,再返回接受c的函数
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}
console.log(curriedAdd(1)(2)(3)); // 6

ES6 箭头函数可以让书写更简洁:

const curriedAdd = a => b => c => a + b + c;

自动柯里化工具

实际项目中很少手写多层嵌套,而是使用通用柯里化函数(如 Lodash 的 _.curry)或自己实现一个:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      // 参数收集够了,直接调用原函数
      return fn.apply(this, args);
    } else {
      // 参数不够,返回一个继续收集参数的函数
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6 (部分应用也是支持的)

实际价值与场景

柯里化的最大价值在于生成特定功能的偏应用函数,提高复用性。

举个例子:日志函数

const log = (level, date, message) =>
  `[${date}] ${level}: ${message}`;

const curriedLog = curry(log);

// 固定日志级别
const errorLog = curriedLog('ERROR');
const warnLog = curriedLog('WARN');

// 固定日期
const todayErrorLog = errorLog('2025-04-07');
const todayWarnLog = warnLog('2025-04-07');

console.log(todayErrorLog('数据库连接失败'));
// [2025-04-07] ERROR: 数据库连接失败
console.log(todayWarnLog('内存使用率超过80%'));
// [2025-04-07] WARN: 内存使用率超过80%

通过柯里化,我们把一个通用的 log 函数一步步“特化”成具体场景下的记录器。某个模块只需引入预处理好的 todayErrorLog,调用时只传消息,代码既简洁又避免参数顺序错误。

另一个常见场景是函数式编程中与 mapfilter 配合:

const multiply = a => b => a * b;
const double = multiply(2);
const triple = multiply(3);

[1, 2, 3].map(double); // [2, 4, 6]
[1, 2, 3].map(triple); // [3, 6, 9]

注意事项

  • 柯里化基于函数的 length 属性(形参数量),如果函数定义了默认参数或剩余参数,length 可能不准确,需要更健壮的实现或使用 Lodash。
  • 过度使用柯里化可能导致调用链难以理解,尤其是在团队不熟悉的情况下。应根据场景适度选用。

9.6.2 偏函数(Partial Application)

定义
偏函数是指固定一个函数的部分参数,生成一个参数更少的新函数。与柯里化不同,偏函数并不要求每次只接收一个参数,它可以一次固定任意数量的参数。

对比

  • 柯里化:一步步拆分,每次只接受一个参数,最终所有参数就位后执行。
  • 偏函数:一次性固定若干参数,返回一个等待剩余参数的新函数。

可以用 Function.prototype.bind 天然实现偏函数,因为 bind 可以为函数预设部分参数:

function sendMessage(from, to, content) {
  console.log(`${from} -> ${to}: ${content}`);
}

// 固定第一个参数 from,偏函数化
const sendFromAdmin = sendMessage.bind(null, 'Admin');
sendFromAdmin('User123', '您的订单已发货');
// Admin -> User123: 您的订单已发货

// 也可固定前两个参数
const adminToUser = sendMessage.bind(null, 'Admin', 'VIP001');
adminToUser('您的优惠券已到期');
// Admin -> VIP001: 您的优惠券已到期

手动实现一个更通用的 partial 函数:

function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn.apply(this, presetArgs.concat(laterArgs));
  };
}

// 使用
const multiply = (a, b, c) => a * b * c;
const multiplyBy2 = partial(multiply, 2);
console.log(multiplyBy2(3, 4)); // 24

实际场景

偏函数特别适合提供默认配置简化接口。例如在发送 AJAX 请求时,可以先固定基础 URL 或公共头部,让后续调用更简洁:

const baseFetch = (baseURL, options, path) =>
  fetch(`${baseURL}${path}`, options);

const apiFetch = partial(baseFetch, 'https://api.example.com', {
  headers: { 'Content-Type': 'application/json' }
});

apiFetch('/users'); // GET https://api.example.com/users
apiFetch('/orders'); // GET https://api.example.com/orders

柯里化可以看作偏函数的一种特殊情况(每次只固定一个参数),但两者的着眼点不同:柯里化倾向于将多参数函数转变为单参数函数链,偏函数则是快速生成一个特定场景的便捷函数。

9.6.3 函数组合(Function Composition)

定义
函数组合是将多个函数合并成一个函数,数据依次流过每个函数,前一个函数的输出作为后一个函数的输入。数学上,若 fg 是两个函数,组合 h = f ∘ g 满足 h(x) = f(g(x))。在编程中,组合通常是从右向左执行。

手动组合

const add5 = x => x + 5;
const double = x => x * 2;
const square = x => x * x;

// 普通嵌套调用(从内向外读,容易乱)
const result = square(double(add5(3)));
console.log(result); // ((3+5)*2)^2 = 256

// 组合函数:先 add5,再 double,最后 square
const compose = (f, g, h) => x => f(g(h(x)));
const transform = compose(square, double, add5);
console.log(transform(3)); // 256

通用 compose 实现

// 从右向左执行
function compose(...fns) {
  return function(initialValue) {
    return fns.reduceRight((acc, fn) => fn(acc), initialValue);
  };
}

// 或者用 reduce(从左向右,称为 pipe)
function pipe(...fns) {
  return function(initialValue) {
    return fns.reduce((acc, fn) => fn(acc), initialValue);
  };
}

ES6 箭头函数优雅写法:

const compose = (...fns) => value => fns.reduceRight((acc, fn) => fn(acc), value);
const pipe = (...fns) => value => fns.reduce((acc, fn) => fn(acc), value);

实际场景

函数组合是构建数据处理管道的利器,尤其在数组转换、字符串处理、中间件模式中极为常见。

示例1:文本处理

const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const addEmoji = str => `${str} 🎉`;

const processText = compose(addEmoji, toLowerCase, trim);
console.log(processText('  HeLLo WorLd  ')); // "hello world 🎉"

示例2:数组过滤和映射

const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 17 },
  { name: 'Charlie', age: 30 }
];

const isAdult = user => user.age >= 18;
const getName = user => user.name;
const greet = name => `Hello, ${name}!`;

const getAdultsGreetings = compose(
  arr => arr.map(greet),
  arr => arr.map(getName),
  arr => arr.filter(isAdult)
);

console.log(getAdultsGreetings(users));
// ['Hello, Alice!', 'Hello, Charlie!']

这里用 compose 把三个步骤无缝连接,每个步骤职责单一,可以独立测试和复用。

与管道操作符的展望

未来 TC39 提案中的管道操作符 |> 可以让函数组合的写法更自然:

// 提案语法(尚未正式纳入标准)
const result = value |> add5 |> double |> square;

目前还需要借助 composepipe 实现类似效果。

9.6.4 三者的关系与选型

  • 柯里化:倾向于将多参数函数拆解为“单参数化”,方便生成中间函数。
  • 偏函数:快捷预设部分参数,简化调用。
  • 函数组合:将多个小函数串联成一个大函数,实现数据流动。

三者常常协同工作:先用柯里化或偏函数生成适配的函数,再用组合将它们组装成完整的处理流程。

const add = a => b => a + b;
const multiply = a => b => a * b;

const add10 = add(10);
const double = multiply(2);

const transform = compose(add10, double);
console.log(transform(5)); // 5*2 + 10 = 20

9.6.5 实用建议

  1. 不要为了函数式而函数式。如果一段逻辑用简单的命令式写法更直白、团队成员更容易理解,就不要刻意转为组合形式。
  2. 优先保证可读性。组合链过长时,可以通过命名中间变量或拆分成多个步骤来提升可读性。
  3. 善用 Lodash / Ramda。这些库提供了开箱即用的 currypartialcomposepipe 等函数,避免重复造轮子。
  4. 注意调试。当组合链中出现错误时,调用栈可能不如普通函数清晰。可以在组合链中插入一个 tap 函数用于打印中间值,或在调试时临时拆解。
const tap = label => value => {
  console.log(label, value);
  return value;
};
const process = compose(
  addEmoji,
  tap('转为小写后'),
  toLowerCase,
  trim
);

理解并掌握这三种模式,你将能够以更函数式的方式组织复杂逻辑,让代码像搭积木一样构建,更容易测试和复用。这也是深入前端框架、状态管理、中间件等高级话题的重要基石。