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,而是先由右侧的5、2确定c、b,最后传入的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 → a、3 → b、2 → c;而curryRight(divide)中2 → c、3 → b、12 → 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 placeholders与should 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('处理用户请求'));timestamp与level属于"每个日志实例都相同"的配置项,只有message变化。通过curryRight,一次柯里化即可派生出errorLog、infoLog、debugLog三个专用函数,避免在每个调用点重复传入时间与级别。
实战场景三:函数式编程流水线
借助curryRight将map、filter、reduce包装为"数据在最后"的版本,可以让组合代码的阅读顺序与执行顺序一致:
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随后补入,最终返回"吃一个数组吐一个变换后数组"的函数,供filterWith、reduceWith继续嵌套。
实战场景四: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开始逐步回填headers、method,最后只剩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:
- 入口归一化(L134-L143):
arity默认取func.length,经Number.parseInt取整,非法值归零;同时保留一个可作迭代器使用的guard参数,供map等集合方法将curryRight直接作为 iteratee 使用(spec 中should work as an iteratee用例见 curryRight.spec.ts)。 - 递归柯里化(L145-L183):每次调用先统计真实参数个数(总参数数减去占位符数);若未达到
arity,则调用makeCurryRight返回新的包装函数继续等待参数;达到目标后,若以new调用则通过new func(...args)构造实例,否则用func.apply(this, args)保持this绑定。 - 参数合成(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。
参数与返回值速查
参数
func(Function):要从右到左柯里化的函数。arity(number,可选):函数的参数个数;省略时使用func.length。
返回值
(Function & { placeholder: symbol }):从右到左柯里化的函数,可通过其placeholder属性控制参数位置(用于占位符)。
结合本文的全部示例与源码分析,你可以根据场景做如下决策:追求最大性能且无占位符需求时,用主库版curryRight或手动闭包;需要兼容 Lodash 行为、占位符、任意拆分传参、arity控制时,用es-toolkit/compat的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
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考