函数是 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,调用时只传消息,代码既简洁又避免参数顺序错误。
另一个常见场景是函数式编程中与 map、filter 配合:
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)
定义
函数组合是将多个函数合并成一个函数,数据依次流过每个函数,前一个函数的输出作为后一个函数的输入。数学上,若 f 和 g 是两个函数,组合 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;
目前还需要借助 compose 或 pipe 实现类似效果。
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 实用建议
- 不要为了函数式而函数式。如果一段逻辑用简单的命令式写法更直白、团队成员更容易理解,就不要刻意转为组合形式。
- 优先保证可读性。组合链过长时,可以通过命名中间变量或拆分成多个步骤来提升可读性。
- 善用 Lodash / Ramda。这些库提供了开箱即用的
curry、partial、compose、pipe等函数,避免重复造轮子。 - 注意调试。当组合链中出现错误时,调用栈可能不如普通函数清晰。可以在组合链中插入一个
tap函数用于打印中间值,或在调试时临时拆解。
const tap = label => value => {
console.log(label, value);
return value;
};
const process = compose(
addEmoji,
tap('转为小写后'),
toLowerCase,
trim
);
理解并掌握这三种模式,你将能够以更函数式的方式组织复杂逻辑,让代码像搭积木一样构建,更容易测试和复用。这也是深入前端框架、状态管理、中间件等高级话题的重要基石。