news 2026/9/17 0:05:11

es-toolkit 函数柯里化指南:使用 curryRight 实现从右向左的参数部分应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
es-toolkit 函数柯里化指南:使用 curryRight 实现从右向左的参数部分应用

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]); }; }

关键逻辑有三点:

  1. 依赖func.length:柯里化的"目标参数个数"取的是func.length(即函数声明中参数的数量,不含 rest 参数)。因此若想正确柯里化,目标函数应使用具名参数而非...rest收集。
  2. 0 或 1 个参数直接原样返回:无参函数本来就能直接调用,单参函数调用一次即完成,柯里化没有意义,因此直接返回func本身。
  3. 首调用只接收一个参数:传入的第一个参数作为初始累积值[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); // 占位符支持

使用注意事项

  1. 依赖func.length:柯里化目标参数个数取自func.length。带默认值、rest 参数或解构参数的函数,其length可能与直觉不符,必要时改用 compat 版本并显式传入arity
  2. 0 参与单参函数会被原样返回:这是源码中明确的行为(src/function/curryRight.ts#L169-L171),调用它们时不会产生新的中间函数。
  3. 一次只能传一个参数:主库curryRight的每次调用只接收一个参数;需要一次传多个参数或使用占位符时,请改用 compat 版本。
  4. 导入路径:主库版本从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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 0:04:03

微信小程序电商源码.zip实战:从解压到上线的完整指南

简介&#xff1a;面向小程序开发者和有电商创业需求的用户&#xff0c;这份微信小程序电商源码合集涵盖了外卖、电商、门店、展示、批发商城、分销等多种业务业态&#xff0c;可帮助读者快速获得可直接参考的小程序前端项目&#xff0c;缩短从零开发到上线的时间。压缩包仅1.96…

作者头像 李华
网站建设 2026/9/17 0:01:05

AI时代创作者突围:如何在算法洪流中保持内容竞争力

1. 当AI开始批量生产内容&#xff1a;创作者面临的全新挑战2023年ChatGPT的爆发式增长彻底改变了内容创作的游戏规则。我亲眼见证了许多同行从最初的"这玩意儿写的东西没人看"到"AI生成内容已经抢走我一半客户"的转变过程。根据SimilarWeb数据&#xff0c;…

作者头像 李华
网站建设 2026/9/17 0:00:15

AWS无服务器应用开发指南:从Lambda到SAM的架构与实践

从一份目录开始&#xff0c;重新理解AWS无服务器应用开发很多人学AWS无服务器&#xff0c;第一反应是去翻Lambda的API文档&#xff0c;或者找几个现成的SAM模板直接部署。这种学法不是不可以&#xff0c;但容易陷入一个怪圈&#xff1a;函数能跑通&#xff0c;却说不清楚整个架…

作者头像 李华
网站建设 2026/9/16 23:58:58

Rerun 图像编码之 ColorModel:颜色分量模型与像素格式定义详解

Rerun 图像编码之 ColorModel&#xff1a;颜色分量模型与像素格式定义详解 【免费下载链接】rerun Visualize, query, and stream to train on multimodal robotics data. 项目地址: https://gitcode.com/GitHub_Trending/re/rerun ColorModel 是 Rerun 数据模型&#x…

作者头像 李华
网站建设 2026/9/16 23:58:06

地理差异化战略:构建企业竞争壁垒的关键

1. 行业地理差异化的战略价值地理差异化&#xff08;Geographic Differentiation&#xff09;正在成为企业构建竞争壁垒的核心策略之一。我在消费品、金融、医疗和制造业四个领域深耕多年&#xff0c;发现即使是同一套商业模式&#xff0c;在不同地理区域落地时会产生惊人的效果…

作者头像 李华
网站建设 2026/9/16 23:57:10

Oracle SQL执行计划看不懂,让Codex改走TaoToken行不行?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华