在函数式编程中,柯里化(Currying) 是一个既优雅又实用的概念。它能让我们将一个多参数函数,转化为一系列只接收单个参数的函数。本文将通过手写代码、逐步拆解,带你彻底掌握 JavaScript 中的柯里化机制——从最简单的 add(1)(2) 到实际项目中的日志封装。
一、什么是柯里化?
柯里化(Currying) :把一个接受多个参数的函数,转换成一系列只接受一个参数的函数,并在所有参数收集完毕后执行原函数。
🌰 最直观的例子:
普通函数:
js
编辑
function add(a, b) {
return a + b;
}
console.log(add(1, 2)); // 3
柯里化后:
function curriedAdd(a) {
return function(b) {
return a + b;
};
}
// 或用箭头函数更简洁
const curriedAdd = (a) => (b) => a + b;
console.log(curriedAdd(1)(2)); // 3
- 先传
1→ 返回一个新函数(b) => 1 + b - 再传
2→ 得到结果3
这就是柯里化的本质:分步传参,延迟执行。
二、柯里化的三大核心机制
1. 闭包(Closure)
每一层函数都能访问外层函数的变量(如 a),这些变量不会被销毁,形成“记忆”。
2. 递归(Recursion)
通过不断返回新函数,实现参数的逐步收集。
3. 退出条件:参数数量到位
如何知道“参数够了”?靠 fn.length —— 函数声明时的形参个数。
function add(a, b, c, d) {
return a + b + c + d;
}
console.log(add.length); // 4
三、手写通用柯里化函数
我们可以写一个工具函数,自动把任意函数转为柯里化形式:
function curry(fn) {
return function curried(...args) {
// 退出条件:当前参数数量 >= 原函数所需参数数量
if (args.length >= fn.length) {
return fn(...args); // 执行原函数
}
// 否则:返回新函数,继续收集参数(闭包 + 递归)
return (...rest) => curried(...args, ...rest);
};
}
🔍 逐行解析:
fn:原始函数(如add)curried(...args):当前已收集的参数if (args.length >= fn.length):判断是否收齐(...rest) => curried(...args, ...rest):合并新旧参数,递归调用
✅ 使用示例:
const add = (a, b, c) => 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
console.log(curriedAdd(1, 2, 3)); // 6
💡 我们的实现兼容“一次传完”或“分多次传”,更灵活!
四、柯里化的实际应用场景
场景 1:创建专用函数(参数复用)
const multiply = (a, b) => a * b;
const curriedMultiply = curry(multiply);
const double = curriedMultiply(2); // 固定第一个参数
const triple = curriedMultiply(3);
console.log(double(5)); // 10
console.log(triple(4)); // 12
避免重复写 multiply(2, x),提升代码复用性。
场景 2:函数语义化(日志系统)
这是前端开发中非常常见的模式:
// 柯里化日志函数
const log = (type) => (message) => {
console.log(`[${type}]: ${message}`);
};
// 固定日志类型,生成专用函数
const errorLog = log("error");
const infoLog = log("info");
const warnLog = log("warn");
// 使用
errorLog("接口异常"); // [error]: 接口异常
infoLog("页面加载完成"); // [info]: 页面加载完成
warnLog("即将弃用该 API"); // [warn]: 即将弃用该 API
✅ 优势:
- 代码更清晰
- 避免每次写
log("error", "...") - 易于维护和扩展
场景 3:React 事件处理(带参数)
// 柯里化风格
const deleteItem = (id) => () => {
// 执行删除逻辑
api.delete(`/items/${id}`);
};
<button onClick={deleteItem(item.id)}>删除</button>
对比普通写法:
// 需要箭头函数包裹
<button onClick={() => deleteItem(item.id)}>删除</button>
柯里化让 JSX 更简洁、性能更好(避免内联回调)。
五、柯里化 vs 偏函数(Partial Application)
| 特性 | 柯里化(Currying) | 偏函数(Partial) |
|---|---|---|
| 传参方式 | 每次只能传 1 个 | 可以传 多个 |
| 返回值 | 总是返回函数,直到参数收齐 | 一次固定部分参数,返回新函数 |
| 示例 | f(a)(b)(c) | f(a, b)(c) 或 f(a)(b, c) |
📌 在实际使用中,界限常模糊。很多库(如 Lodash)的
_.curry也支持多参调用。
六、总结:为什么学柯里化?
| 价值 | 说明 |
|---|---|
| ✅ 参数复用 | 创建配置好的专用函数 |
| ✅ 代码抽象 | 提升函数组合能力 |
| ✅ 语义清晰 | 如 errorLog("...") 比 log("error", "...") 更直观 |
| ✅ 函数式编程基础 | 理解高阶函数、组合、管道等概念的前提 |
⚠️ 注意:不是所有场景都需要柯里化。
如果只是临时计算3 + 5,直接add(3, 5)更简单;
但如果你要创建 10 个“加 10”的函数,那curriedAdd(10)就非常香!
七、动手试试!
现在,你可以自己实现:
// 1. 柯里化一个四则运算函数
const calc = (op, a, b) => {
switch(op) {
case '+': return a + b;
case '*': return a * b;
}
};
// 2. 用 curry 包装它
const curriedCalc = curry(calc);
// 3. 创建专用计算器
const adder = curriedCalc('+');
console.log(adder(5)(3)); // 8
结语
柯里化不是炫技,而是一种思维方式:
“把复杂问题拆解为一系列简单步骤,并在合适的时机组合它们。”
掌握它,你不仅能写出更优雅的 JavaScript,还能更好地理解现代框架(如 React、Redux)和函数式库(如 Ramda、Lodash/fp)的设计哲学。