JavaScript 数组排序要先区分数字、字符串和对象,再决定是否允许修改原数组;数字必须提供比较函数,不想改原数组时优先使用 toSorted 或复制后 sort。

你写 [10, 2, 1].sort(),结果却是 1、10、2;给 React 状态数组调用 sort,页面又出现难追的引用变化。因为默认 sort 会把元素转成字符串比较,而且它会原地修改数组。本文基于 Node.js 24.19.0 实际运行数字、中文字符串、多字段对象和空值四组示例,给出原始输出、选择边界和失败处理。
一、先看结论:四种排序场景怎么选
| 数据与约束 | 推荐写法 | 关键边界 |
|---|---|---|
| 纯数字 | toSorted((a, b) => a - b) |
处理 NaN 与字符串数字 |
| 本地化字符串 | Intl.Collator.compare |
固定 locale 和 options |
| 对象多字段 | 比较器链 | 先主键,再次键 |
| 共享或状态数组 | toSorted / 复制后 sort |
避免原地修改 |
| 空值字段 | 显式判断 null / undefined |
不要只靠空字符串兜底 |
含 NaN 数字 |
显式过滤或兜底 | a - b 可能得到 NaN |
一句话:数字要传比较器,文本要用 localeCompare,对象要拆主次字段,不想改原数组就用 toSorted 或先复制。
二、先记住 sort 的两个默认行为
默认 sort 按字符串的 Unicode 码点顺序比较,所以 10 的第一个字符“1”排在 2 前面。sort 还会返回原数组本身,排序前后的变量指向同一个对象。
const numbers = [10, 2, 1];
// 默认按字符串比较:10 的第一个字符 "1" 排在 "2" 前面
console.log([...numbers].sort());
// 传入数值比较器:按数字大小升序排列
console.log([...numbers].sort((a, b) => a - b));
在 Node.js 24.19.0 中实际运行输出为:
[ 1, 10, 2 ]
[ 1, 2, 10 ]
比较函数返回负数表示 a 在 b 前,正数表示 a 在 b 后,0 表示二者在当前顺序上等价。
| 比较器返回值 | 含义 | 示例 |
|---|---|---|
| 负数 | a 排在 b 前 |
1 |
| 零 | 二者等价 | 0 |
| 正数 | a 排在 b 后 |
-1(若写成 b - a) |
| boolean | 错误用法 | true 会被转成 1,缺少负数分支 |
不要写 (a, b) => a > b,因为 boolean 会被转换成 0 或 1,缺少负值分支,无法满足完整比较规则。
需要补齐数组方法与回调基础时,可以先读 JavaScript 基础教程。真正决定排序质量的是比较函数,而不是 sort 这一个方法名。
三、数字与中文字符串要使用不同比较器
数字升序用 a - b,降序用 b - a。中文、带重音字符或大小写混合文本,直接比较码点通常不符合用户阅读习惯,应使用 localeCompare 或复用 Intl.Collator。
const names = ["前端", "算法", "JavaScript", "编程狮"];
// 创建一次 Collator,避免在比较函数中反复 new
const collator = new Intl.Collator("zh-CN");
// toSorted 返回新数组,不修改 names
const result = names.toSorted((a, b) => collator.compare(a, b));
console.log(result);
toSorted 是 sort 的复制版本,会返回新数组而不修改原数组。
| 方法 | 是否修改原数组 | 返回值 | 兼容性 |
|---|---|---|---|
sort |
是,原地修改 | 原数组引用 | 全环境支持 |
toSorted |
否,返回新数组 | 新数组 | 现代浏览器/Node.js |
[...arr].sort |
否 | 新数组 | 全环境可用 |
现代浏览器已广泛支持 toSorted,但旧运行环境需要先确认兼容性;无法使用时写 [...names].sort(compareFn),不要直接修改共享数组。
Intl.Collator 的创建有成本。大量重复排序时应在外部创建一次并复用,不要在比较函数每次调用时 new。国际化规则也会受 locale 和 options 影响,搜索排序、字典排序和文件名排序未必使用同一套配置。
// 错误示例:每次比较都创建 Collator,性能开销大
// names.sort((a, b) => new Intl.Collator("zh-CN").compare(a, b));
// 正确示例:复用同一个 Collator
const collator2 = new Intl.Collator("zh-CN", { sensitivity: "base" });
names.toSorted((a, b) => collator2.compare(a, b));
四、对象数组先写单字段,再组合多字段
商品先按 category 排序,同分类再按 price 升序,可以用逻辑或连接两个比较结果:
const products = [
{ id: "p1", category: "book", price: 39, name: "算法" },
{ id: "p2", category: "course", price: 99, name: "JavaScript" },
{ id: "p3", category: "book", price: 59, name: "前端" },
];
// 复用 Collator,避免重复创建
const collator = new Intl.Collator("zh-CN");
const sorted = products.toSorted(
// 先比较 category;相等时(||)再比较 price
(a, b) => collator.compare(a.category, b.category) || a.price - b.price
);
console.log(sorted.map(({ id, price }) => `${id}:${price}`).join(","));
console.log(products.map(({ id, price }) => `${id}:${price}`).join(","));
本文实际运行得到:
sorted= p1:39,p3:59,p2:99
original= p1:39,p2:99,p3:59
第一行证明多字段排序生效,第二行证明原数组没有被修改。
| 排序字段组合 | 写法 | 说明 |
|---|---|---|
| 单字段 | (a, b) => a.price - b.price |
最简单 |
| 主字段 + 次字段 | compare(a, b) \|\| a.price - b.price |
主字段相等时用次字段 |
| 三个字段以上 | 拆成具名比较函数 | 避免一行里塞太多规则 |
若排序条件继续增加,可以拆成具名比较函数,或组合多个小比较器,避免一行里塞满空值、日期和文本规则。
// 拆成具名比较器:更容易测试和复用
function byCategoryThenPrice(a, b) {
// 先按分类比较
const categoryResult = collator.compare(a.category, b.category);
// 分类不同时直接返回结果
if (categoryResult !== 0) return categoryResult;
// 分类相同时再按价格升序
return a.price - b.price;
}
const sorted2 = products.toSorted(byCategoryThenPrice);
五、空值、NaN 与稳定排序必须明确规则
接口数据可能缺少 name。直接调用 a.name.localeCompare 会在 null 或 undefined 上报错。先用空字符串兜底只是最简单规则:
const items = [{ name: null }, { name: "编程狮" }, {}];
items.sort((a, b) =>
// 用 ?? 兜底,避免 null/undefined 调用 localeCompare 报错
(a.name ?? "").localeCompare(b.name ?? "", "zh-CN")
);
console.log(items);
Node.js 24.19.0 的实际输出把两个空名称放在前面:
[ { name: null }, {}, { name: '编程狮' } ]
业务若要求空值排最后,就要显式判断,不能把空字符串兜底当成通用答案。
// 空值排最后的写法
items.sort((a, b) => {
// 两个都为空,保持原顺序
if (a.name == null && b.name == null) return 0;
// a 为空,排后面
if (a.name == null) return 1;
// b 为空,排前面
if (b.name == null) return -1;
// 都不为空时按中文比较
return a.name.localeCompare(b.name, "zh-CN");
});
数字字段还要处理 NaN,因为 a - b 可能得到 NaN,比较器无法提供稳定顺序。
| 问题数据 | 直接比较的后果 | 处理方式 |
|---|---|---|
null / undefined 字段 |
调用 localeCompare 报错 |
?? "" 兜底或显式判断 |
NaN 数字 |
比较器返回 NaN,顺序不稳定 |
过滤、兜底或显式判断 |
| 混合类型字段 | 比较结果不可预测 | 先标准化类型再排序 |
| 相等元素 | 依赖稳定排序保持原序 | 现代规范保证稳定,但仍需自洽比较器 |
现代 JavaScript 规范要求 Array.prototype.sort 稳定:比较结果为 0 的元素保持原相对顺序。但稳定排序不等于正确排序;比较函数必须自洽,不能随时间或外部可变状态返回不同结果。
常用数组方法记不牢时,JavaScript 速查手册 适合旁查。调试时先打印排序键,再打印前后数组引用,能快速区分比较规则错误和原地修改问题。
六、失败排查与比较器测试
失败时按三步查:
| 步骤 | 检查内容 | 方法 |
|---|---|---|
| 1 | 元素真实类型 | 打印 typeof 和值 |
| 2 | 比较器返回值 | 单独测试正、负、零三种返回 |
| 3 | 原数组是否被其他代码引用 | 打印排序前后数组引用 |
对于大数组,比较器调用次数很多,应提前解析日期或标准化字段,避免每次比较都做昂贵转换。
把比较器当成独立函数测试
前置条件不是准备一大批数据,而是先选出能暴露规则的最小样例:
| 测试样例 | 验证目标 |
|---|---|
| 小于的一对 | compare(a, b) < 0 |
| 等于的一对 | compare(a, a) === 0 |
| 大于的一对 | compare(a, b) > 0 |
null / undefined |
空值规则明确 |
NaN |
数字字段顺序稳定 |
| 主字段相同 | 次字段给出明确顺序 |
一个正确比较器至少要满足 compare(a, a) === 0,并且 compare(a, b) 与 compare(b, a) 的符号相反。对象多字段排序还要验证主字段相同时,次字段能给出稳定且明确的顺序。
本文数字与对象示例已在 Node.js 24.19.0 实际运行:默认排序得到 [1, 10, 2],数值比较器得到 [1, 2, 10],toSorted 生成新数组且原数组顺序不变。
若浏览器报 toSorted is not a function,这是运行环境支持问题,可升级目标环境或使用 [...items].sort(compareFn);若中文顺序在不同设备不一致,应固定 Intl.Collator 的 locale 与 options,而不要依赖系统默认区域设置。
在提交前再保存一份乱序输入和期望数组,让测试直接比较完整顺序;这比人工扫一眼页面更容易发现相等元素、降序和空值规则被后续改坏。

总结
JavaScript 数组排序的坑主要来自默认字符串比较和原地修改。数字提供减法比较器,文本使用 localeCompare 或 Intl.Collator,对象把规则拆成主次字段,不修改原数组则使用 toSorted 或复制后 sort。
写完比较器后不要只看最终列表,还要保留原数组、空值和相等元素三组样例。排序规则明确、输入类型一致、引用边界可控,结果才适合进入界面状态和业务报表。
延伸学习
- 通过 JavaScript 原生模块课程 把排序函数拆成可测试模块;
- 对照 表格排序实现笔记 理解排序结果如何进入页面;
- 用 JavaScript 开发资源 继续练习数组与控制台调试。
常见问题
Q:sort 为什么把 10 排在 2 前面?
A:没有比较函数时,元素会按字符串形式比较,字符“1”排在“2”前。数字排序要传 (a, b) => a - b。
Q:toSorted 和展开运算符复制后 sort 有什么区别?
A:两者都能避免修改原数组。toSorted 语义更直接;旧环境不支持时,可以使用 [...arr].sort(compareFn) 作为兼容写法。
Q:比较函数可以返回 true 和 false 吗?
A:不应该。比较函数需要负数、零、正数三类结果;boolean 只有 0 和 1,会破坏反向关系并产生不可预测顺序。
Q:对象数组多字段排序怎么写最清楚?
A:先比较主字段,结果不为 0 就直接返回;相等时再比较第二字段。复杂规则拆成具名比较器,比嵌套三元表达式更容易测试。

免费 AI IDE



