在 JavaScript 中调用数组的 sort 方法看似简单,实际上默认排序规则经常和直觉相悖。例如对一个纯数字数组执行 [1, 2, 10].sort(),得到的结果是 [1, 10, 2],而不是预期的 [1, 2, 10]。这并非引擎缺陷,而是由 ECMAScript 规范定义的默认行为:当不传入比较函数时,元素会先被转换为字符串,再按照字符串的 UTF-16 码元值从小到大排列。因此数字 10 的字符串形式 "10" 与 "2" 比较时,先比较第一个字符 "1" 和 "2",由于字符 "1" 的码元值小于 "2",所以 10 排在 2 前面。这个机制也解释了为什么 [1, 2, 10].sort() 会出现看似错误的结果。

要控制排序顺序,需要向 sort 方法传入一个比较函数。比较函数接收两个参数,通常记作 a 和 b,如果返回值小于 0,a 会排在 b 之前;如果返回值大于 0,a 会排在 b 之后;如果返回值等于 0,两个元素的相对位置通常保持不变(但严格稳定需依赖引擎与规范版本)。这个约定看起来简单,却是很多排序需求的基础。
一、sort 方法的默认行为与比较函数机制
如果不传比较函数,sort 并非按数值大小排序,而是调用每个元素的 toString 方法,将元素转换为字符串后按码元顺序比较。对于字符串数组,这种默认行为有时是可接受的;但一旦混杂数字、null、undefined 或对象,结果就可能变得难以预测。例如 [3, 1, 11].sort() 同样会得到 [1, 11, 3],因为字符串 "11" 小于 "3"。
比较函数的核心在于返回值的正负,而不是布尔判断。一个常见的错误是写成 (a, b) => a > b,这会把布尔值转换成 1 或 0,导致无法表达完整的排序关系。正确做法是返回数值,例如升序使用 a - b,降序使用 b - a。对于字符串,则可以使用 localeCompare 或按条件返回 -1、1、0。
const nums = [1, 2, 10]; console.log(nums.sort()); // [1, 10, 2] // 升序 nums.sort((a, b) => a - b); console.log(nums); // [1, 2, 10] // 降序 nums.sort((a, b) => b - a); console.log(nums); // [10, 2, 1]
从上面代码可以看出,同一个数组连续调用 sort 会修改原数组并返回该数组的引用,因此打印 nums 时已经发生变化。如果需要保留原始顺序,应该先复制数组再排序。
二、数字、字符串和对象数组的排序实践
数字排序中最常用的是减法比较器,但要注意浮点数相减可能产生极小的误差,导致比较结果不稳定。例如 0.1 + 0.2 的结果并不是精确的 0.3,在大量浮点数排序时,推荐使用 a - b 已经足够应对一般场景;如果对精度要求较高,可以先缩放为整数,或在比较函数中做条件判断。
字符串排序如果使用默认方式,会按 UTF-16 码元值排序,造成所有大写字母排在小写字母之前,例如 "Cherry" 会排在 "apple" 前面。对于需要忽略大小写或按本地化规则排序的场景,应使用 localeCompare。中文排序也可以借助 localeCompare 并传入 zh-CN 区域参数,按拼音顺序排列。
const fruits = ['banana', 'apple', 'Cherry']; fruits.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); console.log(fruits); // ['apple', 'banana', 'Cherry'] const cities = ['北京', '上海', '广州', '深圳']; cities.sort((a, b) => a.localeCompare(b, 'zh-CN')); console.log(cities); // 按拼音排序
对象数组排序通常需要指定某个字段作为依据。可以直接返回两个对象字段的差值,也可以在差值相同时继续比较下一个字段,实现多级排序。下面示例先按库存升序排列,再按价格升序排列,这样库存相同的商品会按照价格从低到高显示。
const items = [
{ name: '苹果', price: 12, stock: 3 },
{ name: '香蕉', price: 6, stock: 9 },
{ name: '橙子', price: 8, stock: 3 }
];
items.sort((a, b) => {
if (a.stock !== b.stock) {
return a.stock - b.stock;
}
return a.price - b.price;
});
console.log(items.map(i => i.name).join(', ')); // 橙子, 苹果, 香蕉
如果对象中的某个字段可能是空值或字符串,比较前最好做类型转换或空值检查,否则减法可能产生 NaN,导致排序结果不符合预期。实际业务中建议把排序字段先标准化,再进入比较函数。
三、排序稳定性、性能与常见误区
排序稳定性是指当比较函数返回 0 时,相等元素的原始先后顺序是否会被保留。ES2019 起,ECMAScript 规范明确要求 Array.prototype.sort 必须是稳定排序,现代浏览器和 Node.js 的 V8 引擎已经实现这一特性。稳定性在多级排序场景中非常有用,例如先按姓名排序,再按分数排序时,分数相同的人会保持姓名排序后的相对位置。
const students = [
{ name: 'B', score: 80 },
{ name: 'A', score: 90 },
{ name: 'C', score: 80 }
];
students.sort((a, b) => a.score - b.score);
console.log(students.map(i => i.name).join('')); // BAC,稳定排序保留 B 在 C 前
性能方面,sort 方法底层一般使用 TimSort 或快速排序的变体,时间复杂度平均为 O(n log n)。比较函数如果包含复杂计算、DOM 访问或多次类型转换,排序耗时会被显著放大。对于大数据量数组,尽量让比较函数只做简单的数值或字符串比较,可以提前将需要比较的字段映射为简单类型。
常见误区还包括忘记 sort 是原地排序、误用返回布尔值的比较器、以及没有处理数组中的空值。原地排序意味着直接在原数组上修改,如果其他代码仍引用该数组,可能引发意外的数据变化。复制数组可以使用 slice() 或较新的 toSorted 方法。
const original = [3, 1, 2]; const sorted = original.slice().sort((a, b) => a - b); console.log(original); // [3, 1, 2] console.log(sorted); // [1, 2, 3] // 使用 toSorted 返回新数组(需现代浏览器或 Node 20+) const sorted2 = original.toSorted((a, b) => a - b); console.log(sorted2); // [1, 2, 3]
最后再强调一次,比较函数返回布尔值是无效的。布尔值 true 会被强制转换为 1,false 转换为 0,这样比较器只能表示两种状态,无法覆盖负数情况,排序逻辑会变得混乱。只要比较函数返回数值并严格遵守负数、零、正数的约定,JavaScript 数组排序就能保持清晰和可预测。
JavaScript数组排序排序比较函数稳定排序修改时间:2026-09-30 03:32:49