Skip to content
与
柯里化
概述
柯里化(currying)是把一个多参数函数转化成一系列每次只收部分参数的调用链,直到参数收齐才真正执行原函数。不同于 bind 的永久参数预绑定,柯里化在参数齐全时会立即求值。
LeetCode 题目 2632. 柯里化 给出的语义是:编写一个 curry 函数,传入原函数后返回柯里化版本,使得可以通过 csum(1)(2,3)、csum(1,2,3) 等多种方式调用,最终结果与原函数一致。
基本概念
- 参数累积:每次调用柯里化函数都会收集本次传入的参数并与历史参数合并,返回新函数,直到总参数数量不低于原函数的形参个数。
- 求值时机:一旦参数数量满足
fn.length,立即调用原函数并返回结果。 - 灵活调用:单次可以传递任意数量参数,支持混合链式调用。
工作原理
核心实现如下:
ts
function curry(fn: Function): Function {
return function curried(...args: any[]): any {
if (args.length >= fn.length) {
return fn(...args);
}
return function (...nextArgs: any[]) {
return curried(...args.concat(nextArgs));
};
};
}curried 内部闭包捕获两样东西:
- 原函数
fn:来自外层作用域,用于最终求值。 - 已收集的参数
args:每次调用时作为当前函数的参数传入。
参数数量不足时,返回一个新函数,该函数再调用时会将新参数 nextArgs 与已有 args 拼接,递归调用 curried。因为这个递归深度受限于形参总数(例如 3 个参数最多 3 层),不存在堆栈溢出风险。
fn.length 的角色
fn.length 是 JavaScript 函数的 length 属性,表示函数定义中第一个具有默认值之前的形参个数,剩余参数不计入。例如:
function f(a, b, c) {}的length为3。function f(a, b = 1, ...c) {}的length为1(b有默认值,之后的形参都不再计入)。
这个属性在函数创建时就固定了,与调用时实际传入的参数个数无关。柯里化正是用这个值来判断“参数是否足够”。
基本用法
假定有一个求和函数:
ts
function sum(a: number, b: number, c: number) {
return a + b + c;
}经过柯里化后:
ts
const csum = curry(sum);
csum(1)(2)(3); // 6
csum(1, 2)(3); // 6
csum(1)(2, 3); // 6
csum(1, 2, 3); // 6每次调用都累积参数,直到参数总数达到 sum.length(即 3),然后执行 sum 并返回结果。
判断条件是 args.length >= fn.length,所以一次性传入超过形参数量的参数也会立即求值:
ts
csum(1, 2, 3, 4); // 6多出的参数会被忽略,因为原函数只使用了前三个形参。
类型签名(TypeScript)
如果希望柯里化后的函数具有精确的类型推导,可以为 curry 标注类递归类型。下面是一种简化后的声明思路,用于展示泛型如何随着参数数量收缩:
ts
type Curried<F> = F extends (...args: infer A) => infer R
? A extends [infer First, ...infer Rest]
? (arg: First) => Curried<(...args: Rest) => R>
: R
: never;
declare function curry<F extends (...args: any[]) => any>(fn: F): Curried<F>;这样,curry(sum) 的返回值类型会推导为 (a: number) => (b: number) => (c: number) => number。需要注意,这个签名每次只允许传入一个参数;完整支持多参数一次性传入的类型签名会复杂得多,因此上面的声明主要用于理解类型层面的递进收缩,不作为通用声明使用。
注意点
this 丢失
上述实现未绑定 this,导致对象方法在柯里化后失去原始上下文:
ts
const obj = {
factor: 10,
multiply(a, b) {
return (a + b) * this.factor;
}
};
const cm = curry(obj.multiply);
cm(2, 3); // NaN,this 指向 global / undefined(严格模式)如果原函数依赖 this,可以将柯里化版本适配为支持传入 this 或用箭头函数预绑定。最简单的修正是在 curry 内部调用时使用 fn.call 或 fn.apply 传入正确的 this,但通用的柯里化很难自动推断期望的上下文。实际使用时,更常见的做法是避免柯里化依赖 this 的方法,或者在柯里化前就用 bind、箭头函数预先绑定上下文:
ts
const boundCurry = curry(obj.multiply.bind(obj));
boundCurry(2, 3); // 50参数可传递任意数量
由于每次调用都可接收多个参数,柯里化后的函数既能满足通常“每次一个参数”的使用习惯,也接受一次传递多个参数。这使得它既可以作为完全柯里化(unary curry)的近似,又兼容手工批量传参。
限制
fn.length 的计算规则直接影响了柯里化的适用性:
- 默认参数和剩余参数:若函数定义中包含默认参数或剩余参数,
fn.length只会统计到第一个默认参数之前的形参数目。对于function f(a, b = 1, ...c),f.length为1。此时柯里化在拿到一个参数后就会立即求值,后续再传入参数将不再起作用,与原函数语义不符。 - 无法处理变参函数:对于使用
arguments或剩余参数处理任意数量参数的函数,柯里化无法通过fn.length准确判断参数是否齐全。 - 无法中断或重新开始:当前实现一旦开始累积参数就无法重置,不适于流式或可以中途取消的场景。
与 bind 的区别
bind 也支持参数预填充,但它返回一个永久绑定了部分参数的新函数,永远不会自动执行。curry 则在参数足够时立即求值。两种机制对应不同的使用场景:bind 适合固定部分上下文和前置参数的偏函数,curry 适合分步传参、延迟求值的组合式调用。
应用
一些常见的触发场景:
- 偏函数(partial application):预先固定某些参数生成新函数,例如
const add5 = csum(5);之后add5(1, 2)输出8。 - 函数组合与管道:在函数式编程中,将多参数函数转化为单参数函数链,便于在
pipe或compose中接入。 - 事件处理器的参数分离:在注册回调时预先传入上下文或标识符,实际触发时再由框架补充事件对象。
这些场景下需要注意原函数形参数量必须与实际期望的参数总数一致,否则会因为 fn.length 的限制出现提前求值或无法求值。
参考链接
- 2632. 柯里化 – 题目描述与测试用例
- Function.length - MDN –
fn.length的语义和边界情况 - Function.prototype.bind() - MDN – 参数预绑定与柯里化的对比
