
一份接口返回的价格序列,排完序前面几条看着正常,越往后越乱。把数据丢进 node 复现,毛病出在比较器写成了一行 return a > b。这类坑不止一个,按踩中的频率排下来,数字全部来自本机 node v26.8.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,一整轮比下来没有一个负数。 -
返回 NaN 或者什么都不返回,两个元素被当成并列。
['b', 'a', 'c'].sort((x, y) => x - y)一个都没动,因为两个字符串相减是 NaN。字典序要用a.localeCompare(b),引擎把 NaN 和 undefined 都按 0 处理。 -
不传比较器,数字先被转成字符串,再按 UTF-16 码元比大小。
[10, 9, 100, 1, 25, 3].sort()得到 [1, 10, 100, 25, 3, 9],'10' 排在 '9' 前面。按数值排要给(x, y) => x - y。 -
sort 原地改的就是这个数组,并且返回同一个引用。
const out = arr.sort(cmp)里 out === arr 为 true,arr 自己已经变了。要留住原数组用arr.slice().sort(cmp),或者用新的arr.toSorted(cmp),后者返回新数组。 -
undefined 元素一律被摆到末尾,比较器根本收不到它们。
[3, undefined, 1, undefined, 2].sort((x, y) => x - y)得到 [1, 2, 3, undefined, undefined],本机统计比较器收到 undefined 的次数是 0。规范里 undefined 排在所有有值元素之后。 -
稀疏数组的空洞也按同一类处理,排到末尾,但不会变成有值的 undefined。
[3, , 1, , 2].sort((x, y) => x - y)得到带两个空洞的 [1, 2, 3],长度还是 5,有值索引从 0, 2, 4 变成 0, 1, 2。用扩展运算符展开再排会先把空洞填成 undefined,测出来的结果不一样。 -
相等元素想保住原来的先后,比较器必须返回 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 是稳定的,更老的引擎不保证。 -
比较器被调用的次数不是固定的 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 次,次数随数据形状变。 -
字符串照码元比,大写字母排在前面,文件名里的数字段不按大小。
['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 自己定下的规矩,比较器管不了。
拿到一个可疑的排序结果,最快的验证是换一组带重复键的数据再排一遍,看相等元素有没有保持原顺序。我现在的做法是排序前先确认比较器返回的是数字,不是判断。