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 时,排序算法会把该次结果当作零,不能依赖排序过程替你报告非法数值。
修复后用多位数、负数、小数、重复值和空数组检查数值顺序;若要求保留原始顺序,还应核对原数组未被改动。回退排序逻辑前保留原数组或重新读取原始输入,已经排序的数组本身通常不足以还原原来的排列。


