← dns.resolveCname() net.SocketAddress 用法 →

Lodash sortBy用法详解:稳定排序机制、多字段迭代器与原生sort的性能选型对比

著
原创 2026-10-18 Node.js 已有人查阅

一、_.sortBy()在排序工具链中的位置

Lodash的_.sortBy(collection,[iteratees=[_.identity]])接收数组或对象,按迭代器返回值升序排列,返回新数组。它的设计目标是降低排序代码的声明式程度——不需要手写比较函数,只需描述“按什么排”。

它与_.orderBy()构成一对职责分明的工具:sortBy只做升序,orderBy支持方向控制与多字段独立排序方向。Lodash的ESLint插件有一条collection-ordering规则,建议在不需要降序时优先使用sortBy,仅在涉及降序或多方向排序时使用orderBy。这一规则背后的逻辑是:排序意图越简单,代码的可读性越高,出错面越小。

二、语法与迭代器语义

_.sortBy(collection, [iteratees=[_.identity]])

collection可以是数组,也可以是普通对象。传入对象时,Lodash对其值进行排序,返回数组。

iteratees是排序依据,支持多种形式:

形式 示例 含义
省略 _.sortBy([3,1,2]) 按值本身升序
属性名 _.sortBy(users,'age') 按age属性升序
属性名数组 _.sortBy(users,['name','age']) 先name后age,均升序
函数 _.sortBy(items,x=>x.price*x.qty) 按计算结果升序

迭代器数组的排序优先级是从左到右:先比较第一个迭代器的结果,相等时比较第二个,依此类推。这一点与SQL的ORDERBYcol1,col2语义一致。

一个容易被忽略的细节:_.sortBy对每个元素调用迭代器时只传入一个参数(value),不传索引或本身。如果需要基于索引排序,必须预先构造带索引的中间结构。

三、稳定排序:不只是一个“加分项”

Lodash文档明确说明sortBy执行的是稳定排序:相等元素的原始相对顺序会被保留。这一特性在业务代码中经常被无意依赖,却很少被显式声明。

考虑一个场景:从数据库取出的订单列表已按创建时间降序排列,现在需要按状态分组展示,同一状态内保持时间顺序。如果使用非稳定排序,同一状态的订单顺序可能被打乱;而sortBy的稳定排序保证在按status排序后,同状态内的订单仍维持原有的时间序。

原生Array.prototype.sort在ES2019规范中也被要求稳定,但Node.js较老版本或某些运行时环境的实现可能不满足。Lodash的稳定排序是在库层面保证的,不依赖运行时的实现差异。这也是在多环境部署的项目中继续使用Lodash排序的一个实际理由。

四、与_.orderBy()的职责对比

维度 _.sortBy() _.orderBy()
排序方向 仅升序 每个字段可独立指定asc/desc
内部逻辑 轻量,无方向参数解析 需解析方向数组,单字段场景有额外开销
适用场景 单字段或多字段全升序 需要降序、或混合方向的多字段排序

实测数据表明,在单字段升序场景下,sortBy的性能优于orderBy,因为后者需要额外处理排序方向参数;当数据量达到百万级别时,这一差异会变得可测量。但在十万条以下的数据集中,两者的差异通常可以忽略,选型应以代码可读性优先。

我的判断是:如果排序需求不涉及降序,直接用sortBy。代码中不出现"asc"字符串,意图更干净。一旦需要降序,不要用sortBy排序后再reverse()——reverse()会破坏稳定排序对相等元素相对顺序的保证,且多字段混合方向时无法表达。

五、性能特征与原生Array.sort的边界

一个在性能测试平台上的对比结果显示:对同一数组排序,原生Array.prototype.sort约66ops/sec,_.sortBy约6ops/sec。这一差距的根源在于Lodash的封装层:sortBy需要遍历迭代器、构造比较键、调用内部baseOrderBy,而原生sort直接执行传入的比较函数。

但这个数字需要放在正确的语境中理解。_.sortBy的价值不在于“比原生快”,而在于减少手写比较函数的出错概率。原生sort的比较函数有三个经典:

//  1:直接相减在字符串上失效
[10, 9, 100].sort((a, b) => a - b); // [9, 10, 100] 正确
['10', '9', '100'].sort((a, b) => a - b); // 隐式转换后可能出错

//  2:忘记处理相等返回 0
users.sort((a, b) => a.age > b.age ? 1 : -1); // 相等时返回 -1,排序不稳定

//  3:多字段手写比较冗长且易错

_.sortBy将这些细节封装起来,代价是性能损耗。选型判断:数据量在万级以下、排序逻辑涉及多字段或嵌套属性时,sortBy的开发效率优势大于性能损失;数据量达到百万级且为单字段简单排序时,手写原生比较函数是合理的优化;千万级以上应考虑分片排序或交给数据库处理。

六、边界测试与常见错误

6.1混合类型数组的排序行为

原文示例2展示了[4,8,2,"z","a","g","28"]的排序结果为[2,4,8,"28","a","g","z"]。"28"排在数字之后、字母之前,是因为sortBy在比较时会对值做隐式处理:数字之间按数值比较,字符串之间按字典序比较,数字与字符串之间则按某种类型提升规则处理。

这一行为不应被依赖。生产中如果可能包含混合类型,应在排序前显式规范化:

const normalized = arr.map(v => typeof v === 'string' ? v : String(v));
_.sortBy(normalized);

或者更明确地分离类型,分别排序后合并。

6.2属性缺失或值为undefined

const data = [
  { name: 'B', age: 30 },
  { name: 'A' },           // age 缺失
  { name: 'C', age: 25 },
];
_.sortBy(data, 'age');

Lodash会将undefined排在有序值之后。这一行为在Lodash文档中没有显式承诺,不同版本间可能存在差异。如果业务上需要将缺失值视为特定值(如0或最大值),应通过迭代器函数显式处理:

_.sortBy(data, item => item.age ?? Number.MAX_SAFE_INTEGER);

6.3迭代器返回对象或复杂类型

当迭代器返回对象时,sortBy的比较行为不再直观。排序键应是可比较的原始值(数字、字符串、日期时间戳)。如果迭代器返回对象,比较结果可能不符合预期。这是sortBy的隐式契约:迭代器负责提取可比较的标量键。

七、自定义键名与关联结构的排序

sortBy的属性名迭代器支持Lodash的路径语法,可以访问嵌套属性:

const orders = [
  { id: 1, customer: { name: '张三', level: 'B' } },
  { id: 2, customer: { name: '李四', level: 'A' } },
];
_.sortBy(orders, 'customer.level');

对于键名不固定的场景(例如从API动态获取的字段名),迭代器函数是更安全的选择:

function sortByDynamicKey(items, key) {
  return _.sortBy(items, item => item[key] ?? '');
}

对于Map或自定义结构,sortBy本身不支持直接传入,需要先用Array.from(map.entries())转换为数组,再按条目索引或值排序。

八、常见误区速查

误区一:sortBy会修改原数组。不会。它返回新数组,原保持不变。这是与原生sort的关键差异——原生sort原地修改。

误区二:sortBy可以对多字段分别指定升序和降序。不能。sortBy的所有迭代器统一按升序处理。混合方向必须用orderBy。

误区三:sortBy排序后再reverse()等于降序。数值上结果可能正确,但对于相等元素,reverse()会反转它们的原始顺序,破坏了稳定排序的语义。且多字段场景下无法只反转其中某个字段。

误区四:sortBy能替代数据库的ORDERBY。对于分页查询,应用层排序需要先把全量数据取回内存,这在数据量增长后不可扩展。排序应优先在数据存储层完成,sortBy适合处理已取回的小批量结果或内存中的中间数据。

九、项目实例:订单列表的多级排序

const _ = require('lodash');

// 代码号学习编程:订单多级排序
const orders = [
  { id: 'A1', status: 'pending', priority: 2, createdAt: '2026-03-15' },
  { id: 'A2', status: 'shipped', priority: 1, createdAt: '2026-03-10' },
  { id: 'A3', status: 'pending', priority: 1, createdAt: '2026-03-20' },
  { id: 'A4', status: 'shipped', priority: 2, createdAt: '2026-03-12' },
];

// 需求:状态升序(pending 在前),同状态内优先级升序,同优先级内创建时间升序
const sorted = _.sortBy(orders, ['status', 'priority', 'createdAt']);

如果需求变为“同状态内优先级降序”,sortBy无法直接表达。两种选择:改用orderBy,或者对优先级取负值作为排序键:

_.sortBy(orders, ['status', o => -o.priority, 'createdAt']);

取负值的写法在单字段降序时简洁有效,但语义不如orderBy的['asc','desc','asc']直观。团队协作中,当降序字段超过一个时,建议统一使用orderBy,避免负值技巧散落各处增加阅读负担。

十、练习与思考

练习1:创建一个包含{name,score,submittedAt}的对象数组,用_.sortBy按score升序排列,观察相同score的元素是否保持了原数组中的相对顺序。然后将排序结果反转,再比较相同score元素的顺序是否与原始一致。

练习2:写一个函数,接收对象数组和一个字段名,返回按该字段升序排序的新数组,要求缺失该字段的元素排在之后。分别用_.sortBy和原生sort实现,比较代码量。

思考:你的接口返回了一个5000条的用户列表,前端需要按注册时间降序展示。你会选择在Node.js服务端用_.sortBy排序后返回,还是让前端在浏览器中排序?考虑响应体大小、服务端CPU开销和分页场景的适用性。

十一、延伸阅读与参考文献

Lodash官方文档—_.sortBy(collection,[iteratees=[_.identity]])
https://lodash.com/docs/4.17.15#sortBy
最权威的语法与行为说明,明确“执行稳定排序,保留相等元素的原始顺序”。

Lodash官方文档—_.orderBy(collection,[iteratees],[orders])
https://lodash.com/docs/4.17.15#orderBy
与sortBy对照阅读,理解方向参数的设计差异。

eslint-plugin-lodash—CollectionOrdering规则
https://github.com/import-js/eslint-plugin-lodash/blob/master/docs/rules/collection-ordering.md
说明何时用sortBy、何时用orderBy的静态分析规则,可作为团队代码规范的参考依据。

StackOverflow—LodashsortBy稳定排序讨论
https://stackoverflow.com/questions/45153226/how-to-make-lodash-sortby-not-sort-if-values-are-equal
回答直接引用官方文档确认稳定排序特性,附实际排序行为示例。

腾讯云开发者社区—LodashsortBy与orderBy大数据集性能对比
https://cloud.tencent.cn/developer/information/使用sortBy的Lodash链接在普通排序工作时产生错误
基于Node.js18和Lodash4.17.21的测试数据,讨论十万级和百万级数据下的性能差异与选型建议。

MeasureThat—LodashsortByvsArray.prototype.sort基准测试
https://www.measurethat.net/benchmarks/Show/33443/1/lodash-sort-vs-arrayprototypesort-vs-multisort
第三方基准测试结果,可直观看到封装层带来的吞吐量差异。注意测试环境与数据特征不同,数值仅供参考。

← dns.resolveCname() net.SocketAddress 用法 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号