es-toolkit 函数柯里化指南:使用 curryRight 实现从右向左的参数部分应用
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
curryRight是 es-toolkit 提供的柯里化工具函数,它把普通函数改造成"一次只接收一个参数"的链式调用形式,并且从最后一个参数开始接收。本文以 docs/ja/reference/function/curryRight.md 为核心,结合源码实现与测试用例,完整讲解curryRight的用法、与curry的差异、底层原理以及实战场景,读完即可在自己的项目中直接运用。
curryRight 是什么
柯里化(Currying)是一种函数式编程技术:把接收多个参数的函数,改造成"每次调用只接收一个参数、返回一个新函数等待下一个参数"的形式,直到所有参数集齐后才真正执行原函数。
es-toolkit 的curryRight与普通的curry不同——普通curry从**第一个参数(最左侧)开始接收,而curryRight从最后一个参数(最右侧)**开始接收。调用方式如下:
const curriedFunc = curryRight(func);返回值是一个被柯里化后的函数,可以"从右到左、一次一个参数"地调用它。
基本用法
curryRight(func)
当你想从右到左对函数做部分应用时使用curryRight。与普通curry不同,它从最后一个参数开始接收。
import { curryRight } from 'es-toolkit/function'; function sum(a: number, b: number, c: number) { return a + b + c; } const curriedSum = curryRight(sum); // 为参数 `c` 提供值 `10` const add10 = curriedSum(10); // 为参数 `b` 提供值 `15` const add25 = add10(15); // 为参数 `a` 提供值 `5` // 所有参数已集齐,此时返回计算结果 const result = add25(5); // 返回: 30可以看到,参数提供的顺序是c → b → a,正好与函数定义sum(a, b, c)的声明顺序相反。
右侧参数优先更自然的场景
当"先固定靠后的参数、后传入靠前的参数"更符合直觉时,curryRight就特别有用。例如问候语函数greet(greeting, name),先固定姓名、再传入不同的问候语:
function greet(greeting: string, name: string) { return `${greeting}, ${name}!`; } const curriedGreet = curryRight(greet); const greetJohn = curriedGreet('John'); greetJohn('Hello'); // 返回: 'Hello, John!' greetJohn('Hi'); // 返回: 'Hi, John!'这里先通过curriedGreet('John')把name固定下来,得到一个专门问候 John 的函数greetJohn,之后每次传入不同的greeting即可复用,非常符合"先定人、再定话术"的业务直觉。
参数与返回值
- 参数
func((...args: any[]) => any):要被柯里化的函数。 - 返回值(
(...args: any[]) => any):柯里化后的函数,可从右到左一次接收一个参数。
与 curry 的左右对比
es-toolkit 同时提供了curry(左到右)与curryRight(右到左)两个函数,两者的实现结构完全对称,只是参数累积方向相反。用除法函数对比最直观:
import { curry, curryRight } from 'es-toolkit/function'; function divide(a: number, b: number, c: number) { return a / b / c; } // 普通 curry:从左到右,即 (12 / 3) / 2 = 2 const leftCurried = curry(divide); leftCurried(12)(3)(2); // 2 // curryRight:从右到左,最后一个传入的 12 会成为第一个参数 a const rightCurried = curryRight(divide); rightCurried(2)(3)(12); // 2左柯里化按a → b → c顺序接收参数;右柯里化按c → b → a顺序接收参数。最终两者的计算结果一致(都是((12 / 3) / 2) = 2),只是参数的提供顺序不同。
源码级实现原理
curryRight的实现位于 src/function/curryRight.ts。先看核心入口(src/function/curryRight.ts#L168-L176):
export function curryRight(func: (...args: any[]) => any): (...args: any[]) => any { if (func.length === 0 || func.length === 1) { return func; } return function (arg: any) { return makeCurryRight(func, func.length, [arg]); }; }关键逻辑有三点:
- 依赖
func.length:柯里化的"目标参数个数"取的是func.length(即函数声明中参数的数量,不含 rest 参数)。因此若想正确柯里化,目标函数应使用具名参数而非...rest收集。 - 0 或 1 个参数直接原样返回:无参函数本来就能直接调用,单参函数调用一次即完成,柯里化没有意义,因此直接返回
func本身。 - 首调用只接收一个参数:传入的第一个参数作为初始累积值
[arg],交给内部递归函数makeCurryRight。
真正干活的是makeCurryRight(src/function/curryRight.ts#L178-L187):
function makeCurryRight<F extends (...args: any) => any>(origin: F, argsLength: number, args: any[]) { if (args.length === argsLength) { return origin(...args); } else { const next = function (arg: Parameters<F>[0]) { return makeCurryRight(origin, argsLength, [arg, ...args]); }; return next; } }它的工作方式是一个典型的闭包累积器:
- 每次调用检查已累积的参数个数
args.length是否达到目标argsLength; - 未达到:返回一个新的函数,把新传入的参数前置到累积数组,即
[arg, ...args]——正是这个unshift式的前置操作实现了"从右向左"的累积顺序(最先传入的参数排在数组最后,最终传给原函数时它对应最右侧的形参); - 已达到:一次性调用
origin(...args)执行原函数并返回结果。
以curryRight(sum)(10)(15)(5)为例,参数累积过程为[10] → [15, 10] → [5, 15, 10],最终执行sum(5, 15, 10) = 30,与文档示例完全吻合。
与之对称,curry的实现(src/function/curry.ts#L159-L179)在累积时使用[...args, arg]尾部追加,从而保持从左到右的顺序。
类型重载设计
为了让 TypeScript 用户获得精确的类型推导,curryRight在实现之上声明了从 0 参到 5 参共 6 组重载(src/function/curryRight.ts#L17-L116)。以三参函数为例:
export function curryRight<P1, P2, P3, R>( func: (p1: P1, p2: P2, p3: P3) => R ): (p3: P3) => (p2: P2) => (p1: P1) => R;重载的返回类型链(p3: P3) => (p2: P2) => (p1: P1) => R精确刻画了"从右到左逐层接收参数"的调用形态,每一层都能推导出当前应该传入的参数类型,无需任何类型断言。超过 5 个参数的函数会落入兜底的(...args: any[]) => any签名。
测试用例验证
curryRight的行为在 src/function/curryRight.spec.ts 中有完整覆盖,其中三组用例最具代表性:
// 1. 无参函数:原样返回 const fn = () => 'test'; const curried = curryRight(fn); expect(curried()).toBe('test'); // 2. 四参函数:按 4 → 3 → 2 → 1 的从右向左顺序累积 const fn = (a: number, b: number, c: number, d: number) => [a, b, c, d]; const curried = curryRight(fn); expect(curried(1)(2)(3)(4)).toEqual([4, 3, 2, 1]); // 3. 类型推导:每层调用都能推出下一层参数类型 const fn = (a: number, b: string, c: boolean) => ({ a, b, c }); const curried = curryRight(fn); expectTypeOf(curried).parameters.toEqualTypeOf<[boolean]>(); expectTypeOf(curried(true)).parameters.toEqualTypeOf<[string]>(); expectTypeOf(curried(true)('a')).parameters.toEqualTypeOf<[number]>(); expectTypeOf(curried(true)('a')(1)).toEqualTypeOf<{ a: number; b: string; c: boolean }>();前两个用例验证了"0/1 参数直接返回"和"右到左参数累积"的行为;第三个用例通过expectTypeOf验证了重载类型链:第一层接收boolean、第二层接收string、第三层接收number,最终返回完整结果对象。
实战场景:从右向左的部分应用
固定"后置参数",复用"前置参数"
适合把函数末尾的参数当作"配置项"提前固定。例如日志格式化函数logWithPrefix(message, level, timestamp),先固定时间戳与日志级别,得到专属 logger:
import { curryRight } from 'es-toolkit/function'; function logWithPrefix(message: string, level: string, timestamp: string) { return `[${timestamp}] ${level}: ${message}`; } const curriedLog = curryRight(logWithPrefix); // 固定 timestamp(最右侧参数) const currentTimeLog = curriedLog(new Date().toISOString()); // 固定 level,生成不同级别的 logger const errorLog = currentTimeLog('ERROR'); const infoLog = currentTimeLog('INFO'); console.log(errorLog('Database connection failed')); console.log(infoLog('Server started'));数据管道中的"数据后置"
柯里化常与数组高阶函数配合,把"数据"放在参数最后、把"处理函数"放在前面,使部分应用后的函数可以作为管道的一环直接使用:
import { curryRight } from 'es-toolkit/function'; // 注意:把 array 放在参数最右侧 const mapWith = curryRight((array: number[], fn: (x: number) => number) => array.map(fn)); const filterWith = curryRight((array: number[], predicate: (x: number) => boolean) => array.filter(predicate) ); const double = (x: number) => x * 2; const isEven = (x: number) => x % 2 === 0; // 先固定处理函数,之后只要传入数据即可 const doubleAll = mapWith(double); const keepEven = filterWith(isEven); const numbers = [1, 2, 3, 4, 5]; keepEven(doubleAll(numbers)); // [4, 8]手动闭包:更快的替代方案
如果只需固定一两个参数,直接用闭包手写可能比柯里化更直观高效(也符合 es-toolkit 文档对性能的倡导):
// 使用 curryRight const curriedSubtract = curryRight((a: number, b: number, c: number) => a - b - c); // 手动闭包(同样从右到左) const manualCurryRight = (c: number) => (b: number) => (a: number) => a - b - c; console.log(curriedSubtract(1)(2)(5)); // 2 console.log(manualCurryRight(1)(2)(5)); // 2与 compat 版本(Lodash 兼容)的区别
es-toolkit 还在 src/compat/function/curryRight.ts 提供了一版兼容 Lodash 语义的curryRight(从es-toolkit/compat导入),两者适用场景不同,官方在 compat 参考文档 中明确给出了对比与建议:
| 能力 | 主库curryRight(es-toolkit/function) | compatcurryRight(es-toolkit/compat) |
|---|---|---|
| 每次调用接收的参数 | 只能一次一个 | 一次一个或多个均可 |
占位符placeholder | 不支持 | 支持,用于跳过指定位置 |
自定义arity | 不支持(依赖func.length) | 支持,可显式指定 |
| 性能 | 快(无额外校验) | 相对较慢(涉及占位符处理、arity 校验与参数重组) |
compat 版本支持curryRight(1, 2)(3)这种一次传多个参数的调用,也支持用curryRight.placeholder跳过参数位(源码见 src/compat/function/curryRight.ts#L134-L161)。官方建议:不需要占位符时,优先使用主库的curryRight或手写闭包,以获得更快的执行速度。
// 主库版本:更快,但只支持一次一个参数 import { curryRight } from 'es-toolkit/function'; const curriedMain = curryRight((a: number, b: number, c: number) => a + b + c); curriedMain(1)(2)(3); // 支持 curriedMain(1, 2)(3); // 不支持 // compat 版本:灵活,支持占位符,但较慢 import { curryRight as compatCurryRight } from 'es-toolkit/compat'; const curriedCompat = compatCurryRight((a: number, b: number, c: number) => a + b + c); curriedCompat(1, 2)(3); // 支持 curriedCompat(1)(compatCurryRight.placeholder, 3)(2); // 占位符支持使用注意事项
- 依赖
func.length:柯里化目标参数个数取自func.length。带默认值、rest 参数或解构参数的函数,其length可能与直觉不符,必要时改用 compat 版本并显式传入arity。 - 0 参与单参函数会被原样返回:这是源码中明确的行为(src/function/curryRight.ts#L169-L171),调用它们时不会产生新的中间函数。
- 一次只能传一个参数:主库
curryRight的每次调用只接收一个参数;需要一次传多个参数或使用占位符时,请改用 compat 版本。 - 导入路径:主库版本从
es-toolkit/function导入(该命名空间由 src/function/index.ts 统一导出);如需 Lodash 兼容语义则从es-toolkit/compat导入。
小结
curryRight是 es-toolkit 函数式工具集中"从右向左"的柯里化方案,与curry形成左右对称的完整能力。它通过一次一个参数、从最后一个形参开始接收的调用方式,让"固定后置配置、复用前置数据"的编程模式变得自然。理解其基于func.length与[arg, ...args]前置累积的实现原理,可以帮助你在合适的场景下正确选用它,并在追求极致性能时平滑切换到手写闭包或 compat 版本。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考