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

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

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

比较器给出 NaN,排序会把它当成 0

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

比较器给出 NaN,排序会把它当成 0

Array.prototype.sort 的比较器偶尔会算出 NaN。常见原因是把字符串当数字相减,或字段缺失后继续参与算术。排序没有因此报错,数组看起来只是没动。这种现象容易被误读成「稳定排序把异常数据放回了原处」。更准确的说法是:比较器的结果被转换成数值,转换后是 NaN 时,这次比较按 0 处理。

返回值不是数字也会进入转换

比较器的结果并不要求运行时类型必须是 number。sort 会把结果做数值转换,再根据正负号判断先后;转换成 NaN 的结果按 0 处理。0 表示这两个元素在本次比较中等价。undefined 转成 NaN,非数字字符串也是如此,所以这两种返回值也会变成并列判定。null 和 false 转成 0,true 转成 1,数字字符串则能转成对应的数字。

这不意味着所有非数字都静默吞掉。Symbol 与 BigInt 无法按这里需要的方式转成 Number,实测都会抛出 TypeError。所以「非数值结果都当作 0」是错的,边界落在转换结果上:转换成功但得到 NaN,才会继续按 0 比较;转换过程本身抛错,排序就会中断。

下面的输入带有重复键,专门观察比较结果如何影响稳定性。常量 NaN 比较器没有给出任何先后关系,四个对象都被视为并列。本机 Node v26.8.1 保留了输入顺序。稳定排序在这里确实保住原次序,但它保住的是比较器声明为相等的次序,不是替数据修正字段。

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

=== 1. 返回值经过数值转换 ===
输入顺序: a b c d
NaN => a b c d
undefined => a b c d
null => a b c d
false => a b c d
true => a b c d
数字字符串 "-1" => d c b a
非数字字符串 "x" => a b c d
对象 valueOf 返回 -1 => d c b a
一次 NaN 的 Number.isNaN: true
一次 undefined 转 Number: NaN
一次 null 转 Number: 0
一次 "x" 转 Number 是 NaN: true

=== 2. 全部被视作相等时,原顺序保留 ===
NaN 比较器结果: a b c d
原顺序: a b c d
顺序相同: true

=== 3. NaN 只会让对应比较结果成为并列 ===
输入顺序: x c a b
Number(key) 差值比较: x a b c
坏值与数值比较返回 NaN: true

=== 4. Symbol 与 BigInt 返回值会抛错 ===

并列不等于排序正确

混合案例里,第一个对象的 key 是 bad。比较时调用 Number(a.key) - Number(b.key),遇到它就会得到 NaN。输出中的 x a b c 是一次真实运行结果;它不能被解释成 x 已经按数值排好。比较器对含坏值的比较都交出了「相等」,其他元素仍可按数值关系移动,于是数组呈现出一种局部有序、整体未定义的样子。

这里的判断值得说清:稳定性只约束比较器认为相等的元素之间的相对次序。比较器若给不同元素都返回 NaN,排序会把那些比较当作 0;这并没有为它们建立可传递的顺序。比较函数不满足一致性要求时,排序结果不能当成可靠的数据规则。单次输出看着稳定,也无法证明结果合理。

清理坏键,再决定如何排序

不建议在比较器末尾简单写 || 0 来遮住 NaN。这样会把缺失键和格式错误的键悄悄混成并列项,后续排查只会更难。更稳妥的做法是在进入排序前确定坏值策略:拒绝该记录、统一放到末尾,或把缺失值显式定义为相等。策略需要由业务含义决定,不能让 NaN 替它做决定。

如果约定无效键排在末尾,可以把分类判断写进比较器,只有两边都是有效数值时才做减法;一边有效一边无效时明确返回正数或负数;两边都无效时返回 0。若输入类型本来就应是数字,先校验比在比较器里兜底更清楚。排序不负责修补数据。对象返回值也不能简单当成「数字对象」看待,具体会执行什么转换取决于对象的原始值转换行为。实测的 valueOf 返回 -1,所以该比较结果能参与先后判断;若转换方法抛错,异常同样会从 sort 中冒出。比较器最好保持纯函数,别在转换方法里改数据或依赖调用次数,否则同一对元素可能在不同调用中得到不同判定。遇到缺失键时,我会先在输入边界处理,不把并列结果留给排序器猜。

如果字段允许数字字符串,预处理阶段还应统一格式。Number('') 会得到 0,空字符串因此可能混进「有效数字」一类;只做 Number.isNaN(Number(value)) 检查未必符合业务规则。校验需确认允许的类型和文本格式,并对空值作单独约定;通过校验的值再交给比较器。这个边界属于输入契约,不属于排序算法。

前一类比较器错误常见于只返回布尔值,它只提供了 0/1,缺少表示反向顺序的负数。这里的问题更隐蔽:比较器表面上返回了表达式结果,却可能因为转换得到 NaN 而只提供并列关系。调试时应记录比较器的原始结果和转换后的数值;一旦看到 NaN,就先查输入字段,别把稳定排序当成故障原因。

    🤞 分享