先记住这个答案
sort() 不传比较函数时,会把每个元素转成字符串,再按 UTF-16 编码一位一位地比。10 变成 "10",9 变成 "9",比第一位时 "1" 小于 "9",所以 10 排到了前面。要按数值排序就传比较函数:升序写 (a, b) => a - b,降序写 (a, b) => b - a。比较函数返回负数表示 a 在前,正数表示 b 在前,0 表示相等。
- 默认排序按字符串逐位比较,不按数值大小
- 数字升序写
(a, b) => a - b - 比较器看返回值正负,返回布尔值是错的
sort()会改原数组,undefined永远排最后
同一组数字,两种排法
先看默认排序和数值排序放在一起的结果。
const values = [10, 9, 20, 2]
console.log(JSON.stringify([...values].sort())) // 按字符串比
console.log(JSON.stringify([...values].sort((a, b) => a - b))) // 按数值比
console.log(JSON.stringify(values)) // 原数组没动,因为排的是副本查看输出与解释
[10,2,20,9]
[2,9,10,20]
[10,9,20,2]默认排序的结果是 10、2、20、9。比较过程就像查字典:先比第一个字符,"1" 最小所以 10 在最前;"2" 和 "20" 第一位相同,短的排前面;"9" 最大排最后。所以这不是 10 和 9 的特例,只要数字位数不一样,默认排序基本都会出错。
示例里用 [...values] 先复制了一份,是因为 sort() 会直接修改原数组。
比较器为什么不能返回布尔值
很多人会写 arr.sort((a, b) => a > b),在一些小数组上看起来也对,但这是错的写法。
比较器要表达三种结果:a 在前、b 在前、两者相等。布尔值转成数字只有 1 和 0,没有负数,引擎永远得不到「a 应该在前」这个信号。结果取决于引擎用的排序算法和数据的初始顺序,换个数据或换个浏览器就可能排错。
- 对象数组按字段排序
- 业务里更多的是给对象数组排序,比如商品按价格升序:
goods.sort((a, b) => a.price - b.price)。价格相同想再按销量降序,就在返回 0 的时候接着比:a.price - b.price || b.sales - a.sales。 字符串字段不能用减法,减出来是NaN。按姓名排序用a.name.localeCompare(b.name),它会按语言习惯比较,还能处理中文。
容易答错的地方
- 以为数组里全是数字,
sort()就会按数字排 sort()不关心元素是什么类型,没有比较函数就一律转字符串。哪怕数组里全是数字,也要显式传(a, b) => a - b。- 以为稳定排序就能保证数字顺序正确
- ES2019 起
sort()规定必须是稳定排序,意思是比较结果为 0 的元素保持原来的先后顺序。它只管相等元素的相对位置,不会改变「默认按字符串比较」这条规则。 - 数组里有
NaN或null也直接减 a - b只要一边是NaN,结果就是NaN,比较器等于没给出有效结论,排序结果会乱。null减法时会变成 0,也未必是你想要的位置。有脏数据时要先过滤,或者在比较器里单独决定它们排前还是排后。
面试官还会怎么问?
数组里有 undefined 会排在哪?
不管用不用比较函数,undefined 都不参与比较,统一放到数组最后,空位排在 undefined 后面。比较函数根本不会收到 undefined。
sort 和 toSorted 怎么选?
两者比较规则完全一样。区别是 sort() 改原数组并返回原数组,toSorted() 返回一个排好序的新数组,原数组不动。toSorted 是 ES2023 新增的,老环境要先确认支持,不支持就用 [...arr].sort()。
中文或带数字的文件名怎么排?
默认排序会把 "file10" 排在 "file2" 前面。用 Intl.Collator 并打开 numeric 选项就能按人的习惯排:arr.sort(new Intl.Collator('zh', { numeric: true }).compare)。中文按拼音排序同样可以用 localeCompare 或 Intl.Collator 指定语言。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。