es-toolkit/fp 的 orderBy 函数式排序指南:在 pipe 管道中实现多条件升序降序
【免费下载链接】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
导读
es-toolkit/fp模块提供了面向函数式编程(data-last 风格)的工具函数,其中orderBy用于创建一个「根据多个条件与排序方向对对象数组排序」的函数。本文将围绕 docs/ja/fp/reference/orderBy.md 展开,讲解该函数在pipe管道中的典型用法、参数与返回值语义,并结合src/fp/array/orderBy.ts、src/array/orderBy.ts与src/_internal/compareValues.ts的源码,剖析其底层实现原理。读完本文,你将能在自己的pipe数据流中自由组合多条件排序,并理解它与普通版orderBy的差异与选型依据。
一、为什么需要 fp 版的 orderBy
普通版orderBy的调用形态是「数组在前」:orderBy(arr, criteria, orders),一次性接收数组并返回排序结果。而es-toolkit/fp的全部函数都采用data-last(数据置后)设计:先接收配置参数,返回一个等待数据传入的函数。这样每个函数都能作为pipe管道中的一个环节。
文档给出的标准形态如下:
const result = pipe(array, orderBy(criteria, orders));也就是说,orderBy(criteria, orders)本身返回(array: readonly T[]) => T[],由pipe把待排序的数组喂给它。从源码可以看到这一层包装非常薄,本质是对主库函数的柯里化封装(src/fp/array/orderBy.ts):
export function orderBy<T extends object>( criteria: ReadonlyArray<((item: T) => unknown) | keyof T>, orders: ReadonlyArray<'asc' | 'desc'> ): (array: readonly T[]) => T[] { return function (array: readonly T[]): T[] { return orderByToolkit(array, criteria, orders); }; }从源码结构可以看出,fp 版并不重复实现排序逻辑,而是在闭包中记住criteria与orders,真正执行时把数组转交给 src/array/orderBy.ts 中的主库orderBy。这也保证了两种版本的行为完全一致。
二、基本用法:在 pipe 中按单一条件排序
文档中的入门示例是按age字段升序排列用户数组:
import { orderBy, pipe } from 'es-toolkit/fp'; const users = [ { name: 'a', age: 2 }, { name: 'b', age: 1 }, ]; pipe(users, orderBy(['age'], ['asc'])); // => [{ name: 'b', age: 1 }, { name: 'a', age: 2 }]这里orderBy(['age'], ['asc'])先被求值,得到一个「接收数组并返回排序后新数组」的函数;pipe再把users从左到右依次传给管道中的每个函数。这一示例同样出现在单元测试 src/fp/array/orderBy.spec.ts 中,用于验证pipe组合的正确性。
三、参数与返回值详解
文档明确给出了orderBy的完整签名:
criteria(Array<((item: T) => unknown) | keyof T>):用于比较的对象键或选择器函数。既可以直接写属性名(如'age'),也可以传返回任意值的函数(如obj => obj.user),两者可以混用。orders(Array<'asc' | 'desc'>):与criteria一一对应的排序方向。'asc'表示升序,'desc'表示降序。- 返回值(
(array: readonly T[]) => T[]):一个将readonly T[]映射为新的已排序数组的函数。
两个数组在类型上被声明为ReadonlyArray,这意味着配置在创建后不应被修改;排序函数对输入的readonly数组也完全只读,不会原地改动原数组(见下文实现分析)。
四、多条件复合排序与方向复用
orderBy的核心价值在于复合排序:当第一个条件比较结果相等时,继续使用下一个条件决定次序。这一点在原文档与主库文档 docs/reference/array/orderBy.md 中均有说明,主库文档给出了更完整的示例:
import { orderBy } from 'es-toolkit/array'; const users = [ { user: 'fred', age: 48 }, { user: 'barney', age: 34 }, { user: 'fred', age: 40 }, { user: 'barney', age: 36 }, ]; orderBy(users, [obj => obj.user, 'age'], ['asc', 'desc']); // [ // { user: 'barney', age: 36 }, // { user: 'barney', age: 34 }, // { user: 'fred', age: 48 }, // { user: 'fred', age: 40 } // ]可以看到:先按user升序,barney排在fred前;同组内再按age降序,所以barney组中 36 排在 34 前,fred组中 48 排在 40 前。属性名与选择器函数可以混合使用。
另一个值得注意的语义是orders 数量不足时最后一个方向被复用。主库文档中的例子:
orderBy(data, ['a', 'b', 'c'], ['asc', 'desc']); // 'a' 升序,'b' 与 'c' 均按降序这一行为在 src/array/orderBy.ts 的实现中可以得到验证:
const ordersLength = orders.length; for (let i = 0; i < criteria.length; i++) { const order = ordersLength > i ? orders[i] : orders[ordersLength - 1]; const criterion = criteria[i]; const criterionIsFunction = typeof criterion === 'function'; const valueA = criterionIsFunction ? criterion(a) : a[criterion]; const valueB = criterionIsFunction ? criterion(b) : b[criterion]; const result = compareValues(valueA, valueB, order); if (result !== 0) { return result; } } return 0;实现细节值得逐点拆解:
- 不修改原数组:先用
arr.slice()复制一份,再在副本上sort,因此返回的是新数组,原数组保持不变。 - 稳定且可组合的比较循环:依次取出每个
criterion与对应的order。若criteria数量多于orders,超出部分全部使用最后一个方向(orders[ordersLength - 1])。 - 键与函数统一处理:
typeof criterion === 'function'时调用函数取值,否则直接读取a[criterion]属性。 - 短路返回:一旦
compareValues返回非零(即两个元素在该条件下可分出先后),立即作为本次比较结果返回;只有全部条件都相等时才返回0,此时sort保持两个元素的相对顺序。
五、底层比较语义:compareValues 对 null / undefined 的处理
排序的最终裁决由 src/_internal/compareValues.ts 完成。它定义了清晰的空值语义:
- 普通值权重为
0,null权重为1,undefined权重为2; - 因此升序时:普通值 <
null<undefined,同类空值之间视为相等; - 降序并不是独立实现,而是「升序比较时交换两个操作数」(
compareAscending(b, a)),这与 lodash 等库的常见策略一致。
export function compareValues(a: any, b: any, order: 'asc' | 'desc'): 0 | -1 | 1 { return order === 'asc' ? compareAscending(a, b) : compareAscending(b, a); }这意味着即使对象某些字段缺失(值为undefined),排序也不会崩溃,空值会被稳定地推到升序序列的末尾,适合处理来自外部数据源的不完整对象。
六、与普通版 orderBy 的选型建议
原文档给出了一条明确的选型指引:普通代码(非管道组合)中,推荐直接使用主库的orderBy;当需要用pipe串联变换时,才使用 fp 版。因为普通版直接接收数组,少一层柯里化包装,调用更直接;而 fp 版的价值在于「配置一次、数据后到」,可以像其他 fp 操作符一样自由嵌入管道。
fp 版还可与es-toolkit/fp的其他函数自然衔接,例如先filter再orderBy:
import { filter, orderBy, pipe } from 'es-toolkit/fp'; const result = pipe( users, filter(u => u.age >= 18), orderBy(['age'], ['desc']) );pipe会将函数按从左到右的顺序依次应用于数据(见 src/fp/pipe.ts 与 docs/fp/reference/pipe.md),无需中间变量,代码自上而下与执行顺序一致,可读性更好。
七、总结
es-toolkit/fp 的orderBy通过 contenteditable="false">【免费下载链接】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),仅供参考