柯里化(Currying):为什么 add(1)(2)(3) 是合法的?
——将多参数函数转换为单参数函数链,实现参数的“预填充”
“柯里化不是语法糖,而是对‘函数应用’的重新思考。”
一、现象:add(1)(2)(3) 为何不报错?
js
const add = a => b => c => a + b + c;
add(1)(2)(3); // 6这行代码能运行,是因为:
- add(1) 返回一个函数 f1
- f1(2) 返回一个函数 f2
- f2(3) 返回结果 6
但这只是表象。
真正的秘密在于:柯里化改变了我们“看待函数”的方式。
二、什么是柯里化?—— 从“一次喂饱”到“逐步消化”
传统函数:多参数,一次性调用
js
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 必须一次性提供所有参数柯里化函数:单参数,链式调用
js
const add = a => b => c => a + b + c;
add(1)(2)(3); // 参数可以分步提供柯里化(Currying)是将一个多参数函数转换为一系列只接受单个参数的函数的过程。
f(a, b, c) 被转换为 f(a)(b)(c)
三、底层机制:闭包与 [[Environment]] 的魔法
为什么 add(1) 返回的函数还能记住 a=1?
答案是:闭包(Closure)
js
const add = a => { // 第一层:捕获 a
return b => { // 第二层:捕获 b 和 a(通过 [[Environment]])
return c => a + b + c; // 第三层:捕获 c, b, a
};
};当 add(1) 执行时:
- 创建新作用域,a = 1
- 返回内部函数 b => c => a + b + c
- 该函数的 [[Environment]] 指向包含 a=1 的词法环境
后续调用都能访问这个“记忆”。
四、柯里化的真正价值:参数的“预填充”(Partial Application)
柯里化的核心优势不是写法炫酷,而是延迟绑定参数,实现:
1. 函数特化(Specialization)
js
const multiply = a => b => a * b;
const double = multiply(2); // 预填充 a=2
const triple = multiply(3); // 预填充 a=3
double(5); // 10
triple(5); // 15你不需要写 double(x) { return 2 * x } 这样的重复代码。
2. 上下文预置(Context Binding)
js
const urlFor = protocol => domain => resource =>
`${protocol}://${domain}/${resource}`;
const httpsGithub = urlFor('https')('github.com');
httpsGithub('algebra'); // 'https://github.com/algebra'
httpsGithub('monad'); // 'https://github.com/monad'urlFor 抽象了 URL 构建逻辑,你可以预置协议和域名,得到专用构造器。
3. 高阶函数的完美搭档
js
// map 接受单参数函数
[1, 2, 3].map(multiply(2)); // [2, 4, 6]
// 如果 multiply 不是柯里化,你得这样写:
[1, 2, 3].map(x => multiply(2, x));柯里化让函数天然适配 map/filter/reduce 等高阶函数。
五、柯里化 vs 偏应用(Partial Application):本质区别
| 特性 | 柯里化(Currying) | 偏应用(Partial Application) |
|---|---|---|
| 形式 | f(a)(b)(c) | f(a, b) |
| 参数数量 | 每次只接受一个参数 | 可一次提供多个参数 |
| 返回值 | 未完全应用时总是返回函数 | 未完全应用时返回函数 |
| 目的 | 数学上的函数分解 | 实用性的参数预设 |
示例对比
js
// 柯里化
const curriedAdd = a => b => c => a + b + c;
curriedAdd(1)(2)(3);
// 偏应用
const partialAdd = (a, b, c) => a + b + c;
const add5 = _.partial(partialAdd, 5); // 预设 a=5
add5(2, 3); // 10柯里化是一种特殊的偏应用,但偏应用更灵活。
六、如何手动实现一个自动柯里化函数?
js
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return (...nextArgs) => curried(...args, ...nextArgs);
}
};
}
// 使用
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6
curriedSum(1)(2, 3); // 6fn.length 返回函数期望的参数个数,curry 利用它判断是否收集完所有参数。
七、工程实践:柯里化的典型场景
1. 配置驱动的函数
js
const validate = rule => value =>
rule.test(value);
const isEmail = validate(/@/);
const isPhone = validate(/\d{10}/);2. 日志装饰器
js
const logger = prefix => message =>
console.log(`[${prefix}] ${message}`);
const errorLog = logger('ERROR');
errorLog('File not found');3. React 中的事件处理器
js
const handleClick = id => event =>
dispatch({ type: 'DELETE', payload: id });
<button onClick={handleClick(5)}>Delete</button>结语:柯里化是“函数式编程的杠杆”
它让你用少量通用函数,组合出大量专用行为。
add(1)(2)(3) 合法,不是因为 JS 语法允许,
而是因为函数可以携带环境、延迟执行、逐步应用。
这才是柯里化的灵魂:
把“何时提供参数”的控制权,交给调用者。