侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 166 篇文章
  • 累计收到 7 条评论

比较器只返回布尔值,排出来往往不是升序

2026-10-8 / 0 评论 / 8 阅读

比较器只返回布尔值,排出来往往不是升序

一份接口返回的价格序列,排完序前面几条看着正常,越往后越乱。把数据丢进 node 复现,毛病出在比较器写成了一行 return a > b。这类坑不止一个,按踩中的频率排下来,数字全部来自本机 node v26.8.1。

  1. 布尔比较器给不出负数,引擎就不知道谁该往前挪。[10, 9, 2, 1, 100, 3].sort((x, y) => x > y) 原样返回,换成 sort((x, y) => x - y) 才得到 [1, 2, 3, 9, 10, 100]。比较器要用负数表示前一个元素排在后面那个的前面,true 和 false 转成数字只有 1 和 0,一整轮比下来没有一个负数。

  2. 返回 NaN 或者什么都不返回,两个元素被当成并列。['b', 'a', 'c'].sort((x, y) => x - y) 一个都没动,因为两个字符串相减是 NaN。字典序要用 a.localeCompare(b),引擎把 NaN 和 undefined 都按 0 处理。

  3. 不传比较器,数字先被转成字符串,再按 UTF-16 码元比大小。[10, 9, 100, 1, 25, 3].sort() 得到 [1, 10, 100, 25, 3, 9],'10' 排在 '9' 前面。按数值排要给 (x, y) => x - y。

  4. sort 原地改的就是这个数组,并且返回同一个引用。const out = arr.sort(cmp) 里 out === arr 为 true,arr 自己已经变了。要留住原数组用 arr.slice().sort(cmp),或者用新的 arr.toSorted(cmp),后者返回新数组。

  5. undefined 元素一律被摆到末尾,比较器根本收不到它们。[3, undefined, 1, undefined, 2].sort((x, y) => x - y) 得到 [1, 2, 3, undefined, undefined],本机统计比较器收到 undefined 的次数是 0。规范里 undefined 排在所有有值元素之后。

  6. 稀疏数组的空洞也按同一类处理,排到末尾,但不会变成有值的 undefined。[3, , 1, , 2].sort((x, y) => x - y) 得到带两个空洞的 [1, 2, 3],长度还是 5,有值索引从 0, 2, 4 变成 0, 1, 2。用扩展运算符展开再排会先把空洞填成 undefined,测出来的结果不一样。

  7. 相等元素想保住原来的先后,比较器必须返回 0,只回 1 或 -1 会把它们打乱。一组带重复键的对象用 sort((x, y) => x.k - y.k) 实测保持原顺序,排出来是 b d a c e;换成 sort((x, y) => x.k > y.k ? 1 : -1) 从不返回 0,同一个数组排成 d b e c a。这一条依赖引擎的稳定排序,本机 node 26 是稳定的,更老的引擎不保证。

  8. 比较器被调用的次数不是固定的 n-1,别在里面写副作用。[5, 4, 3, 2, 1].sort((x, y) => i++ % 2 ? x - y : y - x) 把计数掺进判据,结果依赖引擎内部的比较顺序。本机 n 为 5 时调了 8 次,n 为 10 时 9 次,n 为 100 时 99 次,次数随数据形状变。

  9. 字符串照码元比,大写字母排在前面,文件名里的数字段不按大小。['banana', 'Apple', 'cherry'].sort() 把 Apple 和 Banana 排到小写前面,['item10', 'item2', 'item1'].sort() 排成 item1, item10, item2。要按数字段排得自己拆出数字,或者改用 Intl.Collator(locale, { numeric: true })。

=== 0. 运行环境 ===
node 版本: v26.8.1
V8 版本: 14.6.202.34-node.28

=== 1. 比较器返回布尔值 ===
原数组: [10, 9, 2, 1, 100, 3]
(x,y)=> x > y   返回类型: boolean
(x,y)=> x > y   sort 结果: [10, 9, 2, 1, 100, 3]
(x,y)=> x - y   sort 结果: [1, 2, 3, 9, 10, 100]
布尔值被转成 1 和 0,两个比较值里没有负数

=== 2. 比较器返回 NaN 或 undefined ===
字符串数组: ["b", "a", "c"]
(x,y)=> x - y   单次返回: NaN
(x,y)=> x - y   sort 结果: ["b", "a", "c"]
(x,y)=> undefined sort 结果: ["b", "a", "c"]
(x,y)=> NaN       sort 结果: ["b", "a", "c"]
数字数组 [3, 1, 2] 用 (x,y)=> NaN 排: [3, 1, 2]
返回 NaN 或 undefined 时引擎按 0 处理,两个元素被视为并列

=== 3. 不传比较器:先转字符串,再按 UTF-16 码元比 ===
数字数组: [10, 9, 100, 1, 25, 3]
sort() 无比较器: [1, 10, 100, 25, 3, 9]
sort((x,y)=> x-y): [1, 3, 9, 10, 25, 100]
'10' < '9' 为 true(比的是首字符 '1' 和 '9')
大小写混排 ["B", "a", "C", "b", "A", "c"] 无比较器: ["A", "B", "C", "a", "b", "c"]

=== 4. sort 原地改数组,并且返回同一个引用 ===
arr 原值: [3, 1, 2]
const out = arr.sort(...) 之后 out === arr : true
arr 现在是: [1, 2, 3]
arr.toSorted((x,y)=> y-x) 结果: [3, 2, 1]
toSorted 之后 arr 仍是: [1, 2, 3]
toSorted 返回的是新数组,和 arr 同一引用吗: false

=== 5. undefined 元素一律排到末尾 ===
原数组: [3, undefined, 1, undefined, 2]
(x,y)=> x-y sort 结果: [1, 2, 3, undefined, undefined]
比较器收到 undefined 的次数: 0
undefined 不进比较,规范直接把它们摆到有值元素后面
规范说 undefined 排在所有定义的属性值之后,且它们之间保持原相对顺序

=== 6. 稀疏数组的空洞 ===
原数组 [3, , 1, , 2] 长度: 5
原数组: [3, <hole>, 1, <hole>, 2]
(x,y)=> x-y sort 结果: [1, 2, 3, <hole>, <hole>]
长度仍是: 5
排序前有值索引: 0, 2, 4
排序后有值索引: 0, 1, 2
空洞排序后仍是空洞,不会因为排到末尾就变成显式的 undefined
用 [...s] 展开再排会先把空洞变成 undefined,测出来的结果不一样

=== 7. 排序稳定性 ===
原顺序: a b c d e
(x,y)=> x.k-y.k 结果: b d a c e(相等键保持原相对顺序)
(x,y)=> x.k>y.k?1:-1 从不返回 0: d b e c a
并列判定被跳过,相等元素的相对顺序不再是原来的样子

=== 8. 比较器被调用的次数与副作用 ===
n=5  比较器调用次数: 8(不是 5-1=4)
n=10 比较器调用次数: 9
n=100 比较器调用次数: 99
把调用序号掺进判据 (x,y)=> i++%2 ? x-y : y-x 结果: [5, 2, 1, 3, 4]
结果取决于引擎内部的比较顺序,换一个数组长度或版本就可能不同

=== 9. 字符串比较器按码元,大小写和数字都受影响 ===
原数组: ["banana", "Apple", "cherry", "apple", "Banana"]
无比较器 (码元序): ["Apple", "Banana", "apple", "banana", "cherry"]
'Apple' 排在小写前面,因为大写字母码元更小
文件名 ["item10", "item2", "item1"] 无比较器: ["item1", "item10", "item2"]
要按数字段排得自己拆数字,或换 Intl.Collator numeric

比较器要给一个带符号的数

把比较器当成一个纯粹的比较:负数表示前一个元素排在前面,零或者正数表示不用挪。这两档给齐,上面九条大多不会踩到。数组会不会原地改,undefined 与空洞怎么落位,字符串按什么顺序比,这些是 sort 自己定下的规矩,比较器管不了。

拿到一个可疑的排序结果,最快的验证是换一组带重复键的数据再排一遍,看相等元素有没有保持原顺序。我现在的做法是排序前先确认比较器返回的是数字,不是判断。

    🤞 分享