DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

JavaScript 数字数组 sort 后,100 排在 2 前面

普通 JavaScript 数组默认按字符串比较,数字排序可能把 100 放到 2 前面。使用数值比较函数,保留原数组,并明确数字字符串、NaN 和稀疏数组的输入边界。

数组中的元素明明都是数字,调用不带参数的 sort() 后却得到这样的顺序:

const values = [1, 2, 10, 20, 100, 200];
values.sort();
// 预期按默认规则得到:[1, 10, 100, 2, 20, 200]

这是普通 JavaScript Array 的默认比较方式:没有比较函数时,元素会转成字符串参与比较。"100" 的第一个字符是 "1",所以排在 "2" 前面。比较不只看第一位;首字符相同时还会继续比较后续字符。

本文针对普通数组中的有限 Number 值。TypedArray、BigInt、日期对象和混合类型数组需要各自确认比较规则,不能直接套用这里的范围。

把数值比较函数传给 sort

升序比较可以使用差值:

const values = [1, 2, 10, 20, 100, 200];
values.sort((a, b) => a - b);
// 预期:[1, 2, 10, 20, 100, 200]

返回负数表示 a 应在 b 前面,正数表示在后面,零表示本次比较相等。降序可将比较函数换成 (a, b) => b - a。这里的箭头函数和 const 需要 ES2015 或之后的相应语法支持;问题本身不是新版本才出现的行为。

不要把比较函数写成 (a, b) => a > b。这种写法返回布尔值,转为数值只有 0 和 1,没有正确表达“小于”这个方向,无法满足排序所需的一致比较关系。某组输入看起来正常,不代表换一组数据或运行环境仍然正确。

排序会改动原数组

sort() 返回的仍是原数组。如果原始顺序用于界面展示、其他计算或回退,排序前先复制:

const original = [20, 1, 10, 2];
const sorted = original.slice().sort((a, b) => a - b);
// original 预期仍为 [20, 1, 10, 2]
// sorted 预期为 [1, 2, 10, 20]

对于这里的数字元素,浅拷贝足以保留原数组的元素顺序。若把方案扩展到对象数组,两个数组中的对象仍可能是相同引用,复制数组并不能隔离对象内部修改。

数字字符串需要另行决定输出类型

接口或表单得到的可能是 "20"、"2",而不是数字。减法比较会尝试数值转换,但这不会把数组中保存的字符串替换成数字;排序完成后,后续代码仍可能遇到类型不一致。

如果数据协议要求数字数组,应在进入排序之前完成校验与转换。对已经承诺是数字的输入,可以用下面的检查拒绝类型不符、NaN 或无穷值:

function sortFiniteNumbers(values) {
  if (!Array.isArray(values) ||
      !values.every((value) => Number.isFinite(value))) {
    throw new TypeError("expected an array of finite numbers");
  }
  return values.slice().sort((a, b) => a - b);
}

这个函数按普通、非稀疏数组设计,调用方仍需保证不存在空槽;every 会跳过空槽。Number.isFinite 不会先把字符串转换成数值,所以 "20" 会被拒绝。调用方应按这个输入约定先处理数据。

不要为了通过校验就对所有输入无条件调用 Number。空字符串等输入也可能被转换成有效数字,是否允许必须由字段规则决定。比较函数返回 NaN 时,排序算法会把该次结果当作零,不能依赖排序过程替你报告非法数值。

修复后用多位数、负数、小数、重复值和空数组检查数值顺序;若要求保留原始顺序,还应核对原数组未被改动。回退排序逻辑前保留原数组或重新读取原始输入,已经排序的数组本身通常不足以还原原来的排列。