🔥自动柯里化:如何让普通函数支持 f(1,2,3) 或 f(1)(2)(3)?
——利用闭包与参数长度检测实现灵活调用
“柯里化不是目的,
灵活性才是。”
一、问题:我们想要“两种调用方式都行”
js
const add = (a, b, c) => a + b + c;
// 我们希望它同时支持:
add(1, 2, 3); // ✅ 经典调用
add(1)(2)(3); // ✅ 柯里化调用
add(1, 2)(3); // ✅ 混合调用
add(1)(2, 3); // ✅ 混合调用这叫 自动柯里化(Auto-currying) 或 智能柯里化。
二、核心思路
- 获取函数的期望参数数量 →
fn.length - 收集参数,直到数量足够
- 利用闭包保存已传参数
- 参数够了就执行,否则返回新函数继续收
三、实现一个通用 autoCurry
js
const autoCurry = (fn) => {
const arity = fn.length; // 函数期望的参数个数
const makeCurried = (collectedArgs = []) => {
return (...args) => {
const newArgs = [...collectedArgs, ...args];
// 参数够了,直接执行
if (newArgs.length >= arity) {
return fn(...newArgs);
}
// 不够,返回函数继续收集
return makeCurried(newArgs);
};
};
return makeCurried();
};四、测试我们的 autoCurry
js
const add = (a, b, c) => a + b + c;
const curriedAdd = autoCurry(add);
curriedAdd(1, 2, 3); // 6
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
curriedAdd()(1, 2, 3); // 6全部通过!
五、进阶:支持默认参数和 rest 参数?
问题:fn.length 不包含默认值或 rest 参数
js
const fn = (a, b = 1, ...rest) => {};
fn.length; // 1 —— 只算必传参数在自动柯里化中,这可能导致“提前执行”。
解决方案(可选):
- 要求用户传入
arity手动指定 - 使用 AST 分析(复杂,运行时成本高)
实践中:大多数工具函数无默认/rest,fn.length 足够。
六、真实应用:让 Lodash/Underscore 函数自动柯里化
js
import { map, filter } from 'lodash';
const cMap = autoCurry(map);
const cFilter = autoCurry(filter);
// 现在可以这样用:
const doubleAll = cMap(x => x * 2);
const evensOnly = cFilter(x => x % 2 === 0);
[1,2,3] |> doubleAll |> evensOnly; // [4, 6]七、优化:缓存中间函数(性能考虑)
每次调用都创建新函数,可能影响性能。
我们可以缓存常见参数组合:
js
const autoCurry = (fn) => {
const arity = fn.length;
const cache = new WeakMap(); // 或普通对象
const makeCurried = (collectedArgs = []) => {
// 尝试从缓存读取
if (cache.has(collectedArgs)) {
return cache.get(collectedArgs);
}
const curried = (...args) => {
const newArgs = [...collectedArgs, ...args];
if (newArgs.length >= arity) {
return fn(...newArgs);
}
return makeCurried(newArgs);
};
// 缓存
cache.set(collectedArgs, curried);
return curried;
};
return makeCurried();
};适用于高频调用的函数。
八、工程建议:何时使用自动柯里化?
推荐场景
- 工具函数库(如自定义
utils.js) - 函数组合(
pipe/compose) - 领域特定语言(DSL)
- React 的
connect/withStyles类型函数
不推荐场景
- 性能关键路径(闭包有开销)
- 构造函数或类方法
- 参数复杂的函数(易混淆)
九、对比:手动柯里化 vs 自动柯里化
| 方式 | 优点 | 缺点 |
|---|---|---|
手动柯里化const add = a => b => c => a+b+c | 精确控制,性能好 | 冗长,不支持多参调用 |
| 自动柯里化 | 灵活,兼容旧代码 | 运行时判断,稍慢 |
推荐:在业务项目中用自动柯里化,平衡灵活性与简洁性。
十、结语:柯里化的真正价值是“部分应用”
自动柯里化的核心价值不是语法炫技,而是:
让你轻松创建“预配置”函数。
js
const urlFor = autoCurry((domain, path, query) =>
`${domain}${path}?${new URLSearchParams(query)}`
);
const apiCall = urlFor('https://api.example.com');
const getUser = apiCall('/users');
const user1 = getUser({ id: 1 });
// 'https://api.example.com/users?id=1'这才是函数式编程的优雅所在:
通过组合和部分应用,构建领域语言。