news 2026/9/16 20:37:49

es-toolkit 的 curryRight 完全指南:从右到左的函数柯里化与 Lodash 兼容实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
es-toolkit 的 curryRight 完全指南:从右到左的函数柯里化与 Lodash 兼容实现

es-toolkit 的 curryRight 完全指南:从右到左的函数柯里化与 Lodash 兼容实现

【免费下载链接】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 中用于从右到左柯里化函数的工具:它会创建一个新函数,从最后一个参数开始、逐个或成批地接收参数,直到收集齐所有参数后调用原函数。本文以 es-toolkit 的 Lodash 兼容版(es-toolkit/compat)为核心,完整讲解其调用方式、与主库版本及curry的差异、占位符(placeholder)机制、arity参数控制,并结合 compat 版源码 与 spec 测试 剖析其底层实现原理与适用场景,帮助你写出可复用的柯里化代码。

概览:两个curryRight,两种设计取向

es-toolkit 仓库中实际存在两个curryRight,它们服务于不同的使用场景:

版本导入路径特点文档
主库版本es-toolkit(src/function/curryRight.ts)只支持一次传一个参数,实现极简、速度快docs/reference/function/curryRight.md
compat 兼容版本es-toolkit/compat(src/compat/function/curryRight.ts)支持占位符、arity校验、任意数量的参数组合,行为与 Lodash 对齐但较慢本文(docs/compat/reference/function/curryRight.md)

官方文档在 compat 参考文档 开头明确给出了一条警告:compat 版因复杂的占位符处理、参数个数验证与参数合成逻辑而运行较慢;如果不需要占位符,应优先使用更快的主库curryRight或手写闭包。这一定位决定了本文的写作基调——先讲清 compat 版"能做什么",再说明"何时不必用它"。

从源码的导出关系看,curryRight同时通过 src/compat/compat.ts、src/browser.ts 和 src/function/index.ts 三条路径对外暴露,你可以按项目需要选择es-toolkit/compat、浏览器构建或主库入口。

基本用法:从最后一个参数开始柯里化

compat 版curryRight的调用形式为:

const curriedFunction = curryRight(func, arity);

其中func是被柯里化的函数,arity(可选)指定函数参数个数,省略时使用func.length

import { curryRight } from 'es-toolkit/compat'; // 基本使用 function subtract(a, b, c) { return a - b - c; } const curriedSubtract = curryRight(subtract); // 从右(最后一个参数)开始柯里化 console.log(curriedSubtract(1)(2)(5)); // 5 - 2 - 1 = 2 console.log(curriedSubtract(1, 2)(5)); // 5 - 2 - 1 = 2 console.log(curriedSubtract(1)(2, 5)); // 2 - 5 - 1 = -4 console.log(curriedSubtract(1, 2, 5)); // 1 - 2 - 5 = -6

观察上例可发现 compat 版的一个重要特性:它允许每次调用传入一个或多个参数,并支持多种拆分方式。第一次调用传入的 1 并不立即确定a,而是先由右侧的52确定cb,最后传入的1才落到a上。这与主库版本"一次只能传一个参数"的行为形成鲜明对比。

curry(从左到右)的对比

用除法函数最能直观体现两种柯里化方向的区别:

import { curry, curryRight } from 'es-toolkit/compat'; function divide(a, b, c) { return a / b / c; } // 普通 curry(从左) const leftCurried = curry(divide); console.log(leftCurried(12)(3)(2)); // ((12 / 3) / 2) = 2 // curryRight(从右) const rightCurried = curryRight(divide); console.log(rightCurried(2)(3)(12)); // ((12 / 3) / 2) = 2 // 最后传入的 12 成为第一个参数(a)

curry(divide)12 → a3 → b2 → c;而curryRight(divide)2 → c3 → b12 → a。两种方向最终都得到相同的计算结果2,但参数的接收顺序完全相反。主库版本在 src/function/curryRight.ts 的实现中每次调用仅接收一个参数,并通过[arg, ...args]前插的方式累积参数;compat 版则在此基础上支持多参数与占位符,代价是额外的composeArgs参数合成开销。

与主库版本的对比

// compat 版本(灵活但较慢) import { curryRight } from 'es-toolkit/compat'; const curriedCompat = curryRight(subtract); curriedCompat(1, 2)(3); // 支持 curriedCompat(1)(curryRight.placeholder, 3)(2); // 占位符支持 // 主库版本(更快,但一次只能一个) import { curryRight } from 'es-toolkit'; const curriedMain = curryRight(subtract); curriedMain(1)(2)(3); // 支持 curriedMain(1, 2)(3); // 不支持

选择建议:追求性能、无需占位符时用主库版;需要迁移 Lodash 代码、依赖占位符或灵活传参时用 compat 版。

占位符(placeholder):跳过任意参数位置

占位符是 compat 版curryRight的核心差异化能力。通过curryRight.placeholder(一个默认值为symbol的特殊值,见 src/compat/function/curryRight.ts),你可以预先固定任意位置的参数,并把"空位"留给后续调用填充:

import { curryRight } from 'es-toolkit/compat'; function formatMessage(name, action, time) { return `${name} 在 ${time} 执行了 ${action}`; } const curriedFormat = curryRight(formatMessage); // 用占位符跳过特定位置 const todayAction = curriedFormat('今天'); const todayLoginAction = todayAction(curryRight.placeholder, '登录'); console.log(todayLoginAction('张三')); // "张三 在 今天 执行了 登录" // 先固定时间 const morningFormat = curriedFormat('上午9点'); console.log(morningFormat('发表评论', '李四')); // "李四 在 上午9点 执行了 发表评论"

第一个示例中,curriedFormat('今天')固定了最右侧的time参数;随后用curryRight.placeholder跳过name位置、同时固定action = '登录';最后一次调用todayLoginAction('张三')时,占位符位置被自动填入'张三'。整个过程不需要按照从左到右的声明顺序传参,这正是"右柯里化 + 占位符"组合的价值所在。

从源码看,占位符的解析发生在 composeArgs 函数 中:它统计已累积参数中占位符的数量,用新传入的参数按序回填占位符空位,未用尽的新参数则追加到参数列表末尾。对应的测试覆盖在 curryRight.spec.ts(should support placeholdersshould persist placeholders),后者验证了占位符在多次调用间持续生效:

const curried = curryRight(fn); const ph = curried.placeholder; expect(curried(4)(2, ph)(1, ph)(3)).toEqual([1, 2, 3, 4]); expect(curried('a', ph, ph, ph)('b')(ph)('c')('d')).toEqual(['a', 'b', 'c', 'd']);

实战场景一:数组处理

从右柯里化天然适合"先固定尾部参数"的数据处理模式:

import { curryRight } from 'es-toolkit/compat'; // 从数组末尾取出指定数量的元素 function takeFromEnd(array, count, separator = ', ') { return array.slice(-count).join(separator); } const curriedTake = curryRight(takeFromEnd); // 先固定分隔符 const takeWithComma = curriedTake(', '); // 再固定数量 const takeLast3 = takeWithComma(3); const fruits = ['苹果', '香蕉', '橙子', '葡萄', '猕猴桃']; console.log(takeLast3(fruits)); // "橙子, 葡萄, 猕猴桃" // 使用不同分隔符 const takeWithDash = curriedTake(' - '); console.log(takeWithDash(2, fruits)); // "葡萄 - 猕猴桃"

这里takeFromEnd(array, count, separator)的声明顺序是"数组在前、配置在后",而curryRight让你可以先注入配置(分隔符、数量),最后才提供数据,从而派生出takeLast3这样可复用的专用函数。这正是部分应用(partial application)的典型收益。

实战场景二:函数组合与日志系统

将"变化最小"的参数放在函数声明的最右侧,就能用curryRight构建稳定的专用函数:

import { curryRight } from 'es-toolkit/compat'; // 日志输出函数 function logWithPrefix(message, level, timestamp) { return `[${timestamp}] ${level}: ${message}`; } const curriedLog = curryRight(logWithPrefix); // 固定当前时间 const currentTimeLog = curriedLog(new Date().toISOString()); // 按级别创建 logger const errorLog = currentTimeLog('ERROR'); const infoLog = currentTimeLog('INFO'); const debugLog = currentTimeLog('DEBUG'); console.log(errorLog('数据库连接失败')); console.log(infoLog('服务器启动')); console.log(debugLog('处理用户请求'));

timestamplevel属于"每个日志实例都相同"的配置项,只有message变化。通过curryRight,一次柯里化即可派生出errorLoginfoLogdebugLog三个专用函数,避免在每个调用点重复传入时间与级别。

实战场景三:函数式编程流水线

借助curryRightmapfilterreduce包装为"数据在最后"的版本,可以让组合代码的阅读顺序与执行顺序一致:

import { curryRight } from 'es-toolkit/compat'; const mapWith = curryRight((array, fn) => array.map(fn)); const filterWith = curryRight((array, predicate) => array.filter(predicate)); const reduceWith = curryRight((array, reducer, initial) => array.reduce(reducer, initial)); const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const double = x => x * 2; const isEven = x => x % 2 === 0; const sum = (acc, val) => acc + val; // 组合流水线(右侧优先) const processNumbers = nums => { return reduceWith(filterWith(mapWith(nums, double), isEven), sum, 0); }; console.log(processNumbers(numbers)); // 所有数翻倍 → 过滤偶数 → 求和

由于curryRight固定的是最右侧参数,mapWith(nums, double)nums先被确定,double随后补入,最终返回"吃一个数组吐一个变换后数组"的函数,供filterWithreduceWith继续嵌套。

实战场景四:API 请求构建器

柯里化常被用于"分步配置"请求参数:

import { curryRight } from 'es-toolkit/compat'; function makeRequest(url, method, headers, body) { return fetch(url, { method, headers, body }); } const curriedRequest = curryRight(makeRequest); // 先设置 body const withJsonBody = curriedRequest(JSON.stringify({ data: 'test' })); // 添加 headers const withHeaders = withJsonBody({ 'Content-Type': 'application/json', Authorization: 'Bearer token123', }); // 设置 POST 方法 const postRequest = withHeaders('POST'); // 最终使用 postRequest('/api/data') .then(response => response.json()) .then(data => console.log(data));

makeRequest(url, method, headers, body)的声明顺序与构建过程恰好相反:curryRight允许你从body开始逐步回填headersmethod,最后只剩url一个可变入口。这比手写一个每次都要传四个参数的函数更符合"配置一次、复用多次"的实际诉求。

指定 arity:控制柯里化深度

当函数的func.length不能准确反映期望的参数个数(例如存在 rest 参数或默认参数)时,可以通过第二个参数显式指定:

import { curryRight } from 'es-toolkit/compat'; function variableArgsFunction(a, b, c, ...rest) { return { a, b, c, rest }; } // 将参数个数限制为 3(忽略 rest) const curriedFixed = curryRight(variableArgsFunction, 3); // 从右往左依次接收 c, b, a console.log(curriedFixed(3)(2)(1)); // { a: 1, b: 2, c: 3, rest: [] }

compat 版对arity的规范化逻辑位于 curryRight 实现:arity默认取func.length,随后经过Number.parseInt转为整数,若结果为NaN或小于 1 则归零。spec 中should coerce 'arity' to an integer用例验证了'0'0.6'xyz'等异常值均会被安全归一化(curryRight.spec.ts)。同时注意,源码注释明确说明该方法不会为柯里化函数设置length属性should create a function with a 'length' of '0'用例对此有断言(curryRight.spec.ts)。

底层原理:compat 版如何工作

compat 版curryRight的完整执行流程可概括为三个阶段,全部证据来自 src/compat/function/curryRight.ts:

  1. 入口归一化(L134-L143):arity默认取func.length,经Number.parseInt取整,非法值归零;同时保留一个可作迭代器使用的guard参数,供map等集合方法将curryRight直接作为 iteratee 使用(spec 中should work as an iteratee用例见 curryRight.spec.ts)。
  2. 递归柯里化(L145-L183):每次调用先统计真实参数个数(总参数数减去占位符数);若未达到arity,则调用makeCurryRight返回新的包装函数继续等待参数;达到目标后,若以new调用则通过new func(...args)构造实例,否则用func.apply(this, args)保持this绑定。
  3. 参数合成(composeArgs,L185-L208):将新传入参数按序回填到累积参数中的占位符空位,多余的参数追加到末尾,从而支持任意顺序与任意拆分方式的传参。

与主库版 src/function/curryRight.ts 相比:主库版对func.length为 0 或 1 的函数直接返回原函数(免去无意义的包装),并且每次只接收一个参数、用[arg, ...args]前插累积,无占位符、无arity校验,因此更快;compat 版则为了 Lodash 兼容付出了占位符过滤、整数强制转换与composeArgs合成的额外开销——这正是文档警告"慢"的原因所在。

此外,spec 还验证了若干工程细节:should ensure 'new curried' is an instance of 'func'保证柯里化函数可作为构造函数使用(curryRight.spec.ts);should use 'this' binding of function验证与bind组合时的this传递(curryRight.spec.ts);should work with partialed methods验证与partial/partialRight的协作(curryRight.spec.ts)。

手动柯里化:更快的替代方案

文档特别提示:当不需要占位符与灵活传参时,手写闭包通常是最快的选择

// 使用 curryRight const curriedSubtract = curryRight((a, b, c) => a - b - c); // 手动闭包(更快,从右) const manualCurryRight = c => b => a => a - b - c; // 两者结果相同 console.log(curriedSubtract(1)(2)(5)); // 2 console.log(manualCurryRight(1)(2)(5)); // 2

手动闭包把"柯里化"完全展开成显式的嵌套箭头函数,没有占位符过滤、没有参数合成、没有arity校验,运行时开销趋近于零。如果你的函数签名固定、参数顺序明确,这通常是更优解;只有在需要 Lodash 兼容行为或占位符能力时,才应选用 compat 版curryRight

参数与返回值速查

参数

  • funcFunction):要从右到左柯里化的函数。
  • aritynumber,可选):函数的参数个数;省略时使用func.length

返回值

Function & { placeholder: symbol }):从右到左柯里化的函数,可通过其placeholder属性控制参数位置(用于占位符)。

结合本文的全部示例与源码分析,你可以根据场景做如下决策:追求最大性能且无占位符需求时,用主库版curryRight或手动闭包;需要兼容 Lodash 行为、占位符、任意拆分传参、arity控制时,用es-toolkit/compatcurryRight

【免费下载链接】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/16 20:37:07

WRF定制Vtable接入ERA5-Land土壤湿度全流程详解

WRF跑通一个案例不难,真正烦的是换数据源之后,WPS的ungrib组件开始闹脾气。ungrib能不能把一份GRIB文件里你想要的变量解出来,全看Vtable这张“变量翻译表”对不对得上。我从第一次配Vtable踩坑到现在,至少手改过五六份Vtable&…

作者头像 李华
网站建设 2026/9/16 20:36:56

SEO入门指南:从零开始掌握搜索引擎优化

1. 从零开始理解SEO的核心价值我第一次接触SEO是在2012年运营个人博客时,当时发现同样的内容,有的文章阅读量能过万,有的却只有几十次点击。这个现象让我开始深入研究搜索引擎优化(SEO)的奥秘。SEO本质上是通过对网站内…

作者头像 李华
网站建设 2026/9/16 20:34:07

Claude Code 配 TaoToken:整合 DeepSeek-V4

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

作者头像 李华
网站建设 2026/9/16 20:33:53

GEE遥感分析:remapping与palette高效应用指南

1. Google Earth Engine(GEE)扩展功能深度解析作为一名长期使用GEE进行遥感分析的从业者,我发现remapping和palette这两个看似简单的功能在实际项目中能解决80%的影像分类后处理需求。今天我就结合NLCD(美国国家土地覆盖数据库&am…

作者头像 李华
网站建设 2026/9/16 20:33:34

PIC单片机SPI外设详解:从MSSP寄存器配置到W25Q64实战

简介:SPI是一种应用广泛的同步串行通信协议,常用于微控制器与外围设备之间的高速数据交换;这是一份面向PIC微控制器的SPI通信示例程序包,用C语言编写,主要解决PIC主机与各类SPI从设备之间的数据通信问题。对于需要驱动…

作者头像 李华