手写柯里化 curry
问题
实现一个 curry 函数,将多参数函数转化为一系列单参数函数的链式调用。例如 curry(fn)(1)(2)(3) 等价于 fn(1, 2, 3)。
答案
什么是柯里化
柯里化(Currying) 是一种将接受多个参数的函数转化为一系列接受单个参数的函数的技术。它以数学家 Haskell Curry 的名字命名,是函数式编程中的核心概念之一。
// 普通函数
function add(a: number, b: number, c: number): number {
return a + b + c;
}
add(1, 2, 3); // 6
// 柯里化后
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6(自动柯里化还支持多参数)
curriedAdd(1)(2, 3); // 6
柯里化的本质是利用闭包逐步收集参数,当参数数量达到原函数的形参个数时,才真正执行原函数。
柯里化 vs 偏函数(Partial Application)
很多人容易混淆柯里化和偏函数,它们的区别如下:
| 特性 | 柯里化(Currying) | 偏函数(Partial Application) |
|---|---|---|
| 定义 | 将 f(a, b, c) 转化为 f(a)(b)(c) | 固定函数的部分参数,返回接受剩余参数的函数 |
| 参数转化 | 每次只接受一个参数(严格模式) | 可以一次固定任意数量参数 |
| 返回值 | 返回单参数函数链 | 返回接受剩余参数的函数 |
| 示例 | curry(f)(1)(2)(3) | partial(f, 1, 2)(3) |
// 偏函数:固定前几个参数
function partial<T extends any[], R>(
fn: (...args: T) => R,
...presetArgs: Partial<T>
): (...remainingArgs: any[]) => R {
return (...remainingArgs: any[]) => {
return fn(...([...presetArgs, ...remainingArgs] as T));
};
}
const add3 = (a: number, b: number, c: number) => a + b + c;
const add5And = partial(add3, 5); // 固定第一个参数为 5
add5And(3, 2); // 10
函数式编程中的意义
柯里化在函数式编程中有以下重要作用:
- 参数复用 -- 创建预配置函数,避免重复传参
- 延迟执行 -- 参数不够时不执行,等待所有参数就绪
- 函数组合 -- 使函数签名统一,便于
compose/pipe组合 - Point-Free 风格 -- 减少中间变量,让代码更声明式
基础实现
function curry(fn: Function): Function {
return function curried(this: any, ...args: any[]): any {
// 当收集的参数数量 >= 原函数形参数量时,执行原函数
if (args.length >= fn.length) {
return fn.apply(this, args);
}
// 否则返回一个新函数,继续收集参数
return function (this: any, ...moreArgs: any[]): any {
return curried.apply(this, [...args, ...moreArgs]);
};
};
}
fn.length:Function.length返回函数形参的个数,是柯里化判断参数是否收集完毕的核心依据- 闭包:每次调用返回的新函数通过闭包保存了之前收集的
args this转发:使用apply确保 this 指向正确传递- 递归收集:参数不够时递归返回新函数,直到参数达标才执行
执行流程分析
const add = (a: number, b: number, c: number) => a + b + c;
const curriedAdd = curry(add); // fn.length = 3
// 调用 curriedAdd(1)
// args = [1],1 < 3 → 返回新函数
// 调用 returned(2)
// moreArgs = [2] → curried.apply(this, [1, 2])
// args = [1, 2],2 < 3 → 返回新函数
// 调用 returned(3)
// moreArgs = [3] → curried.apply(this, [1, 2, 3])
// args = [1, 2, 3],3 >= 3 → fn.apply(this, [1, 2, 3]) → 6
测试用例
const add = (a: number, b: number, c: number): number => 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
// this 绑定测试
const obj = {
base: 10,
add: curry(function (this: { base: number }, a: number, b: number) {
return this.base + a + b;
}),
};
console.log(obj.add(1)(2)); // 13
支持占位符的柯里化
在实际使用中,有时我们希望跳过某些参数,先传后面的参数。这时需要引入占位符(Placeholder) 机制,类似 Lodash _.curry 的 _ 占位符。
const PLACEHOLDER = Symbol('curry.placeholder');
function curry(fn: Function): CurriedFunction {
function curried(this: any, ...args: any[]): any {
// 计算实际参数数量(排除占位符)
const realArgs = args.filter((arg) => arg !== PLACEHOLDER);
if (realArgs.length >= fn.length) {
return fn.apply(this, args);
}
return function (this: any, ...moreArgs: any[]): any {
// 用 moreArgs 填充 args 中的占位符
const merged: any[] = [];
let moreIndex = 0;
for (const arg of args) {
if (arg === PLACEHOLDER && moreIndex < moreArgs.length) {
merged.push(moreArgs[moreIndex++]);
} else {
merged.push(arg);
}
}
// 追加 moreArgs 中剩余的参数
while (moreIndex < moreArgs.length) {
merged.push(moreArgs[moreIndex++]);
}
return curried.apply(this, merged);
};
}
curried.placeholder = PLACEHOLDER;
return curried as CurriedFunction;
}
// 挂载到 curry 上便于外部访问
interface CurriedFunction {
(...args: any[]): any;
placeholder: typeof PLACEHOLDER;
}
curry.placeholder = PLACEHOLDER;
占位符使用示例
const _ = curry.placeholder;
const fn = curry((a: number, b: number, c: number, d: number) => [a, b, c, d]);
// 跳过第一个参数
fn(_, 2, 3, 4)(1); // [1, 2, 3, 4]
// 跳过第二个参数
fn(1, _, 3)(2)(4); // [1, 2, 3, 4]
// 跳过多个参数
fn(_, _, 3)(_, 2)(1)(4); // [1, 2, 3, 4]
// 一次填充多个占位符
fn(_, _, _, 4)(1, _, 3)(2); // [1, 2, 3, 4]
占位符版本的 curry 逻辑较复杂,面试中通常不会直接要求写占位符版本。但了解其原理有助于理解 Lodash 的 _.curry 实现,以及加深对函数式编程的理解。
TypeScript 类型安全版本
在基础版中,curry 的返回类型是 Function,丢失了所有类型信息。下面我们用 TypeScript 的条件类型和泛型实现类型安全的 curry。
// 辅助类型:从参数元组的头部逐个"消费"参数
type CurriedFunction<P extends any[], R> =
P extends [infer First, ...infer Rest]
? Rest extends []
? (arg: First) => R // 最后一个参数,直接返回结果
: (arg: First) => CurriedFunction<Rest, R> // 还有剩余参数,返回柯里化函数
: R; // 无参函数,直接返回结果
function curry<P extends any[], R>(
fn: (...args: P) => R
): CurriedFunction<P, R> {
function curried(this: any, ...args: any[]): any {
if (args.length >= fn.length) {
return fn.apply(this, args as P);
}
return function (this: any, ...moreArgs: any[]) {
return curried.apply(this, [...args, ...moreArgs]);
};
}
return curried as CurriedFunction<P, R>;
}
类型推导效果
const add = (a: number, b: string, c: boolean): string => `${a}-${b}-${c}`;
const curriedAdd = curry(add);
// 类型推导:(arg: number) => (arg: string) => (arg: boolean) => string
const step1 = curriedAdd(1); // (arg: string) => (arg: boolean) => string
const step2 = step1('hello'); // (arg: boolean) => string
const result = step2(true); // string → "1-hello-true"
// 类型错误示例(IDE 会报错):
// curriedAdd('wrong'); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
// step1(123); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
上面的类型只支持严格单参数柯里化。如果还要支持 curriedAdd(1, 'hello')(true) 这种多参数调用,类型会变得极其复杂,需要用到递归条件类型和元组操作。在实际项目中,推荐直接使用 Lodash 的类型定义 @types/lodash。
支持多参数传递的完整类型(进阶)
// 从元组 T 中移除前 N 个元素
type DropFirst<T extends any[], N extends any[]> =
N extends [any, ...infer NRest]
? T extends [any, ...infer TRest]
? DropFirst<TRest, NRest>
: []
: T;
// 取元组 T 的前 N 个元素(最多取到 T.length)
type TakeFirst<T extends any[], N extends any[] = []> =
N['length'] extends T['length']
? T
: T extends [...infer Rest, any]
? TakeFirst<T, [...N, any]> extends infer R
? R
: never
: [];
// 支持多参数的柯里化类型
type Curried<P extends any[], R> =
P extends []
? R
: P extends [infer First]
? (arg: First) => R
: P extends [infer First, ...infer Rest]
? ((...args: P) => R) & ((arg: First) => Curried<Rest, R>)
: never;
进阶:自动柯里化
基础的 curry 已经支持了自动柯里化(允许一次传多个参数)。但有一种常见面试题是要求实现无限柯里化,即 add(1)(2)(3)... 可以无限链式调用,最终通过隐式类型转换得到结果。
function add(...args: number[]): any {
let sum = args.reduce((acc, val) => acc + val, 0);
function innerAdd(...moreArgs: number[]): any {
sum += moreArgs.reduce((acc, val) => acc + val, 0);
return innerAdd;
}
// 利用隐式类型转换获取最终值
innerAdd.valueOf = () => sum;
innerAdd.toString = () => String(sum);
return innerAdd;
}
// 测试
console.log(+add(1)(2)(3)); // 6
console.log(+add(1, 2)(3, 4)(5)); // 15
console.log(`${add(1)(2)(3)(4)}`); // "10"
// 也可以用 == 触发隐式转换(不推荐用 ===)
console.log(add(1)(2)(3) == 6); // true
当 JavaScript 需要将对象转为原始值时,会调用 valueOf() 或 toString() 方法。+obj 会优先调用 valueOf(),模板字符串 `${obj}` 会调用 toString()。这是实现无限柯里化的关键,详见 ES6+ 新特性中关于 Symbol.toPrimitive 的说明。
实际应用场景
1. 参数复用 -- 创建预配置函数
const log = curry(
(level: string, module: string, message: string): void => {
const time = new Date().toISOString();
console.log(`[${time}] [${level}] [${module}] ${message}`);
}
);
// 创建预配置的日志函数
const errorLog = log('ERROR');
const userError = errorLog('UserModule');
const authError = errorLog('AuthModule');
userError('用户名不能为空'); // [2026-02-28T...] [ERROR] [UserModule] 用户名不能为空
authError('Token 已过期'); // [2026-02-28T...] [ERROR] [AuthModule] Token 已过期
2. 延迟执行
const validate = curry(
(rule: RegExp, errorMsg: string, value: string): string | true => {
return rule.test(value) ? true : errorMsg;
}
);
// 预定义校验规则
const isEmail = validate(/^[\w.-]+@[\w.-]+\.\w+$/);
const isPhone = validate(/^1[3-9]\d{9}$/);
// 预定义校验规则 + 错误消息
const emailRequired = isEmail('请输入有效的邮箱地址');
const phoneRequired = isPhone('请输入有效的手机号');
// 最终校验
emailRequired('test@example.com'); // true
emailRequired('invalid'); // "请输入有效的邮箱地址"
phoneRequired('13800138000'); // true
3. 函数组合(compose/pipe)
// pipe:从左到右组合函数
const pipe = (...fns: Function[]) =>
(value: any) => fns.reduce((acc, fn) => fn(acc), value);
const map = curry(
<T, U>(fn: (item: T) => U, arr: T[]): U[] => arr.map(fn)
);
const filter = curry(
<T>(predicate: (item: T) => boolean, arr: T[]): T[] => arr.filter(predicate)
);
// Point-Free 风格:无需中间变量
const getActiveUserNames = pipe(
filter((user: { active: boolean; name: string }) => user.active),
map((user: { active: boolean; name: string }) => user.name)
);
const users = [
{ name: 'Alice', active: true },
{ name: 'Bob', active: false },
{ name: 'Charlie', active: true },
];
getActiveUserNames(users); // ['Alice', 'Charlie']
4. React 事件处理
// 柯里化的事件处理器
const handleChange = curry(
(field: string, setState: Function, event: React.ChangeEvent<HTMLInputElement>) => {
setState((prev: any) => ({ ...prev, [field]: event.target.value }));
}
);
// JSX 中使用
// <input onChange={handleChange('username')(setFormState)} />
// <input onChange={handleChange('email')(setFormState)} />
// 更常见的简化写法
const handleFieldChange = (field: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
setFormState((prev) => ({ ...prev, [field]: e.target.value }));
};
// <input onChange={handleFieldChange('username')} />
5. Redux 中间件模式
// Redux middleware 本质就是三层柯里化
const logger: Middleware = (store) => (next) => (action) => {
console.log('dispatching', action);
const result = next(action);
console.log('next state', store.getState());
return result;
};
// 等价于:
const loggerUncurried = (store: any, next: any, action: any) => {
console.log('dispatching', action);
const result = next(action);
console.log('next state', store.getState());
return result;
};
与相关概念对比
| 概念 | 定义 | 参数处理 | 典型用例 |
|---|---|---|---|
| 柯里化 | f(a, b, c) → f(a)(b)(c) | 逐个消费参数 | 参数复用、函数组合 |
| 偏函数 | 固定部分参数,返回接受剩余参数的函数 | 固定任意数量参数 | bind、预配置 |
| 函数组合 | 将多个函数串联为一个:compose(f, g)(x) = f(g(x)) | 传递上一步返回值 | 数据管道、中间件 |
| 高阶函数 | 接受/返回函数的函数 | 函数作为参数或返回值 | map、filter、装饰器 |
// curry + compose = 强大的函数组合能力
const compose = (...fns: Function[]) =>
(value: any) => fns.reduceRight((acc, fn) => fn(acc), value);
const multiply = curry((a: number, b: number) => a * b);
const add = curry((a: number, b: number) => a + b);
// 创建计算公式:(x + 10) * 2
const formula = compose(multiply(2), add(10));
formula(5); // 30
formula(10); // 40
常见面试问题
Q1: 手写一个基础的 curry 函数
答案:
柯里化的核心实现思路:通过闭包递归收集参数,当参数数量满足原函数要求时执行。
function curry(fn: Function): Function {
return function curried(this: any, ...args: any[]): any {
if (args.length >= fn.length) {
// 参数够了,执行原函数
return fn.apply(this, args);
}
// 参数不够,返回新函数继续收集
return function (this: any, ...moreArgs: any[]): any {
return curried.apply(this, [...args, ...moreArgs]);
};
};
}
// 验证
const sum = (a: number, b: number, c: number) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
console.log(curriedSum(1, 2, 3)); // 6
执行流程(以 curriedSum(1)(2)(3) 为例):
Q2: 柯里化的核心原理是什么?
答案:
柯里化依赖三个核心机制:
-
闭包:每次调用返回的新函数通过闭包保留之前收集的参数。闭包让内部函数可以访问外部函数作用域中的变量
args,即使外部函数已经执行完毕。 -
递归收集参数:当传入的参数数量不足时,返回一个新函数。新函数被调用时,将新参数与已有参数合并(
[...args, ...moreArgs]),再次判断参数是否充足。 -
fn.length判断:Function.length表示函数声明的形参个数,用来判断参数是否收集完毕。当收集的参数数>= fn.length时,调用原函数。
function curry(fn: Function): Function {
return function curried(this: any, ...args: any[]): any {
// 原理3:fn.length 判断参数是否充足
if (args.length >= fn.length) {
return fn.apply(this, args);
}
// 原理1:闭包保存 args
// 原理2:递归返回新函数收集参数
return function (this: any, ...moreArgs: any[]): any {
return curried.apply(this, [...args, ...moreArgs]);
};
};
}
Q3: 柯里化和偏函数有什么区别?
答案:
| 维度 | 柯里化(Currying) | 偏函数(Partial Application) |
|---|---|---|
| 转化方式 | f(a, b, c) → f(a)(b)(c) | f(a, b, c) → f(a, b)(c) |
| 参数数量 | 严格模式下每次只接受 1 个参数 | 可一次固定任意数量参数 |
| 实现原理 | 递归收集,fn.length 判断 | 绑定部分参数,返回新函数 |
| JS 原生支持 | 无原生实现 | Function.prototype.bind 就是偏函数 |
| 典型库函数 | _.curry | _.partial、bind |
function greet(greeting: string, name: string): string {
return `${greeting}, ${name}!`;
}
// 偏函数:用 bind 固定第一个参数
const sayHello = greet.bind(null, 'Hello');
sayHello('Alice'); // "Hello, Alice!"
sayHello('Bob'); // "Hello, Bob!"
// 柯里化
const curriedGreet = curry(greet);
const sayHi = curriedGreet('Hi');
sayHi('Alice'); // "Hi, Alice!"
柯里化是将 N 元函数变成 N 个一元函数的嵌套;偏函数是固定若干参数后产出一个参数更少的函数。
Q4: 实现一个支持占位符的 curry
答案:
const PLACEHOLDER = Symbol('placeholder');
function curryWithPlaceholder(fn: Function) {
function curried(this: any, ...args: any[]): any {
// 检查是否所有位置都已填充(无占位符)且参数数量足够
const complete =
args.length >= fn.length &&
args.slice(0, fn.length).every((arg) => arg !== PLACEHOLDER);
if (complete) {
return fn.apply(this, args.slice(0, fn.length));
}
return function (this: any, ...moreArgs: any[]): any {
const merged: any[] = [];
let moreIdx = 0;
// 遍历已有参数,将占位符替换为新参数
for (let i = 0; i < args.length; i++) {
if (args[i] === PLACEHOLDER && moreIdx < moreArgs.length) {
merged.push(moreArgs[moreIdx++]);
} else {
merged.push(args[i]);
}
}
// 追加剩余新参数
while (moreIdx < moreArgs.length) {
merged.push(moreArgs[moreIdx++]);
}
return curried.apply(this, merged);
};
}
return curried;
}
// 测试
const _ = PLACEHOLDER;
const fn = curryWithPlaceholder(
(a: number, b: number, c: number) => [a, b, c]
);
console.log(fn(1, 2, 3)); // [1, 2, 3]
console.log(fn(_, 2, 3)(1)); // [1, 2, 3]
console.log(fn(_, _, 3)(1)(2)); // [1, 2, 3]
console.log(fn(_, 2)(_, 3)(1)); // [1, 2, 3]
占位符核心逻辑:在合并参数时,遍历已有参数列表,如果遇到占位符就用新参数替换;遍历结束后,将多余的新参数追加到末尾。
Q5: fn.length 在柯里化中的作用?什么情况下 fn.length 不准确?
答案:
Function.length 返回函数第一个具有默认值的参数之前的形参个数。它是柯里化判断"参数是否收集完毕"的关键。
以下情况 fn.length 不准确:
// 1. 默认参数:只计算默认值之前的参数
function withDefault(a: number, b = 10, c: number) {}
console.log(withDefault.length); // 1(只有 a 被计算)
// 2. rest 参数:不计算 rest
function withRest(a: number, ...rest: number[]) {}
console.log(withRest.length); // 1(rest 不计算)
// 3. 解构参数:算一个参数
function withDestructure({ a, b }: { a: number; b: number }) {}
console.log(withDestructure.length); // 1(解构整体算一个)
// 4. 完整对比
const cases = [
[(a: number, b: number, c: number) => {}, 3], // 正常:3
[(a: number, b = 1, c: number) => {}, 1], // 默认参数:1
[(a: number, ...rest: number[]) => {}, 1], // rest:1
[(...args: number[]) => {}, 0], // 纯 rest:0
[({ x, y }: { x: number; y: number }) => {}, 1], // 解构:1
] as const;
如果原函数使用了默认参数或 rest 参数,柯里化可能无法正确工作。解决方案是让 curry 接受一个额外参数来手动指定参数数量:
function curry(fn: Function, arity: number = fn.length): Function {
return function curried(this: any, ...args: any[]): any {
if (args.length >= arity) {
return fn.apply(this, args);
}
return (...moreArgs: any[]) => curried.apply(this, [...args, ...moreArgs]);
};
}
// 手动指定参数数量
const add = curry((...nums: number[]) => nums.reduce((a, b) => a + b, 0), 3);
add(1)(2)(3); // 6
Q6: 如何实现 add(1)(2)(3) === 6 且 add(1, 2, 3) === 6?
答案:
这就是"自动柯里化"的需求,基础版 curry 已经支持。关键在于判断参数时使用 >= 而非 ===:
function curry(fn: Function): Function {
return function curried(this: any, ...args: any[]): any {
// 使用 >= 而非 ===,允许一次传多个参数
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (this: any, ...moreArgs: any[]): any {
// 合并参数而非替换
return curried.apply(this, [...args, ...moreArgs]);
};
};
}
const add = curry((a: number, b: number, c: number) => a + b + c);
// 以下所有调用方式都返回 6
console.log(add(1)(2)(3)); // 6 — 逐个传参
console.log(add(1, 2)(3)); // 6 — 先传两个
console.log(add(1)(2, 3)); // 6 — 后传两个
console.log(add(1, 2, 3)); // 6 — 一次全传
Q7: 实现一个无限柯里化 add(1)(2)(3)...最终求和
答案:
由于无限柯里化不知道何时结束调用,无法靠 fn.length 判断。解决方案是利用 JavaScript 的隐式类型转换机制,通过重写 valueOf 或 toString 来获取最终值。
function add(...args: number[]): any {
let sum = args.reduce((acc, val) => acc + val, 0);
function innerAdd(...moreArgs: number[]): any {
sum += moreArgs.reduce((acc, val) => acc + val, 0);
return innerAdd; // 返回自身,支持无限链式调用
}
// 隐式类型转换
innerAdd.valueOf = () => sum;
innerAdd.toString = () => String(sum);
// 也可以用 Symbol.toPrimitive(更标准)
innerAdd[Symbol.toPrimitive] = (hint: string) => {
if (hint === 'number') return sum;
return String(sum);
};
return innerAdd;
}
// 测试(注意需要触发隐式转换)
console.log(+add(1)(2)(3)); // 6
console.log(+add(1)(2)(3)(4)(5)); // 15
console.log(+add(1, 2, 3)(4, 5)(6)); // 21
console.log(`结果: ${add(1)(2)(3)}`); // "结果: 6"
// 用于比较时
console.log(add(1)(2)(3) == 6); // true
// 注意:=== 不会触发隐式转换
console.log(add(1)(2)(3) === 6); // false(类型不同)
无限柯里化 add(1)(2)(3) 的返回值是函数,不是数字。只有在需要类型转换的上下文中(+、==、模板字符串等)才会得到数值。面试中要向面试官明确这一点。
Q8: 柯里化在 React 中有哪些应用?
答案:
柯里化在 React 中有三种典型应用场景:
1. 事件处理器工厂
// 避免在 JSX 中写内联箭头函数
const handleClick = (id: number) => (event: React.MouseEvent) => {
event.preventDefault();
console.log(`Clicked item: ${id}`);
};
// JSX 使用
// <button onClick={handleClick(item.id)}>Delete</button>
// 等价于:<button onClick={(e) => { e.preventDefault(); console.log(...) }}>
2. 高阶组件(HOC)
// withAuth 是柯里化的 HOC
const withAuth = (requiredRole: string) => (WrappedComponent: React.ComponentType) => {
return function AuthGuard(props: any) {
const { user } = useAuth();
if (user?.role !== requiredRole) return <Redirect to="/login" />;
return <WrappedComponent {...props} />;
};
};
// 使用
const AdminPage = withAuth('admin')(DashboardComponent);
const UserPage = withAuth('user')(ProfileComponent);
3. Redux 中间件
// Redux middleware 是典型的三层柯里化
// (storeAPI) => (next) => (action) => result
const thunkMiddleware =
({ dispatch, getState }: MiddlewareAPI) =>
(next: Dispatch) =>
(action: any) => {
// 如果 action 是函数,执行它并传入 dispatch 和 getState
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
Q9: 柯里化的性能问题?
答案:
柯里化虽然优雅,但确实存在性能开销,需要在实际项目中权衡使用:
| 性能维度 | 影响 | 严重程度 |
|---|---|---|
| 闭包开销 | 每次柯里化调用创建新函数和闭包,增加内存占用 | 中 |
| 调用栈深度 | N 个参数 = N 层函数调用,增加调用栈深度 | 低 |
| 垃圾回收压力 | 短生命周期的中间函数增加 GC 压力 | 低-中 |
| V8 优化失效 | 多层闭包可能导致 V8 内联优化失效 | 低 |
// 直接调用 — 最快
const directResult = add(1, 2, 3);
// 柯里化调用 — 有额外开销
const curriedResult = curriedAdd(1)(2)(3);
// 每个 () 都创建了一个新函数对象和闭包
最佳实践:
- 热路径避免柯里化:高频调用的函数(如循环内、动画帧、事件处理)不适合柯里化
- 预先柯里化:在模块初始化时创建柯里化函数,而非运行时动态创建
- 合理使用:配置类、组合类场景适合柯里化;计算密集型场景直接调用
- 适度抽象:不要为了柯里化而柯里化,可读性优先
// 推荐:模块加载时预先柯里化
const formatDate = curry(
(locale: string, format: string, date: Date): string => {
return new Intl.DateTimeFormat(locale, { dateStyle: format as any }).format(date);
}
);
const formatCN = formatDate('zh-CN');
const formatCNLong = formatCN('long');
// 不推荐:循环内动态柯里化
for (const item of largeArray) {
const process = curry(heavyCompute); // 每次循环都创建新的柯里化函数
process(item)(config);
}
Q10: 用 TypeScript 实现类型安全的 curry
答案:
实现类型安全的 curry 需要用到 TypeScript 的条件类型、infer 关键字和递归类型。
// 递归类型:将 (a, b, c) => R 转为 (a) => (b) => (c) => R
type Curry<F extends (...args: any[]) => any> =
F extends (...args: infer P) => infer R
? P extends [infer First, ...infer Rest]
? Rest extends []
? (arg: First) => R
: (arg: First) => Curry<(...args: Rest) => R>
: R
: never;
function curry<F extends (...args: any[]) => any>(fn: F): Curry<F> {
return function curried(this: any, ...args: any[]): any {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (this: any, ...moreArgs: any[]) {
return curried.apply(this, [...args, ...moreArgs]);
};
} as Curry<F>;
}
// 类型验证
const multiply = (a: number, b: number, c: number): number => a * b * c;
const curriedMultiply = curry(multiply);
// 推导类型:(arg: number) => (arg: number) => (arg: number) => number
const step1 = curriedMultiply(2); // (arg: number) => (arg: number) => number
const step2 = step1(3); // (arg: number) => number
const result = step2(4); // number → 24
// IDE 会提示类型错误:
// curriedMultiply('2'); // Error!
// step1(true); // Error!
在面试中能写出类型安全版本的 curry 是很大的加分项,它展示了对 TypeScript 类型系统(条件类型、infer、递归类型)的深入理解。即使写不出完整版本,能说出思路也很好。
Q11: 手写 compose 和 pipe 函数,配合柯里化使用
答案:
// compose:从右向左执行
function compose<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
return (value: T): T => fns.reduceRight((acc, fn) => fn(acc), value);
}
// pipe:从左向右执行
function pipe<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
return (value: T): T => fns.reduce((acc, fn) => fn(acc), value);
}
// 配合柯里化使用
const add = curry((a: number, b: number): number => a + b);
const multiply = curry((a: number, b: number): number => a * b);
// (5 + 10) * 2 = 30
const calculate = pipe(add(10), multiply(2));
console.log(calculate(5)); // 30
// 实际应用:数据处理管道
const trim = (s: string) => s.trim();
const toLower = (s: string) => s.toLowerCase();
const split = curry((sep: string, s: string): string[] => s.split(sep));
const processInput = pipe(trim, toLower);
processInput(' Hello World '); // "hello world"
Q12: 柯里化如何处理 this 绑定?
答案:
柯里化实现中的 this 绑定需要特别注意。必须使用 function 声明(非箭头函数),并通过 apply 正确传递 this。
function curry(fn: Function): Function {
// 必须用 function,不能用箭头函数(箭头函数没有自己的 this)
return function curried(this: any, ...args: any[]): any {
if (args.length >= fn.length) {
return fn.apply(this, args); // 转发 this
}
return function (this: any, ...moreArgs: any[]): any {
return curried.apply(this, [...args, ...moreArgs]); // 转发 this
};
};
}
// 验证 this 绑定
const calculator = {
base: 100,
add: curry(function (this: { base: number }, a: number, b: number): number {
return this.base + a + b;
}),
};
console.log(calculator.add(1)(2)); // 103(this.base = 100)
// 如果用箭头函数实现 curry,this 会丢失
const wrongCurry = (fn: Function) => (...args: any[]) => {
// 箭头函数捕获外层 this,不是调用时的 this
if (args.length >= fn.length) return fn(...args);
return (...more: any[]) => wrongCurry(fn)(...args, ...more);
};
const calc2 = {
base: 100,
add: wrongCurry(function (this: any, a: number, b: number) {
return this.base + a + b; // this 为 undefined!
}),
};
// calc2.add(1)(2) — TypeError: Cannot read property 'base' of undefined
相关链接
- MDN - Function.length
- MDN - Function.prototype.bind
- Lodash - _.curry
- Lodash - _.partial
- Wikipedia - Currying
- 闭包 - 柯里化的核心机制
- this 指向 - 柯里化中的 this 绑定
- ES6+ 新特性 - 展开运算符、箭头函数等