JavaScript数组排序怎么写?sort比较器、toSorted与四种场景

编程狮 2026-09-20 10:38:54 浏览数 (16)
反馈

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

JavaScript 数组排序封面

你写 [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 ]

比较函数返回负数表示 ab 前,正数表示 ab 后,0 表示二者在当前顺序上等价。

比较器返回值 含义 示例
负数 a 排在 b 前 1
二者等价 0
正数 a 排在 b 后 -1(若写成 b - a
boolean 错误用法 true 会被转成 1,缺少负数分支

不要写 (a, b) => a > b,因为 boolean 会被转换成 01,缺少负值分支,无法满足完整比较规则。

需要补齐数组方法与回调基础时,可以先读 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);

toSortedsort 的复制版本,会返回新数组而不修改原数组。

方法 是否修改原数组 返回值 兼容性
sort 是,原地修改 原数组引用 全环境支持
toSorted 否,返回新数组 新数组 现代浏览器/Node.js
[...arr].sort 新数组 全环境可用

现代浏览器已广泛支持 toSorted,但旧运行环境需要先确认兼容性;无法使用时写 [...names].sort(compareFn),不要直接修改共享数组。

Intl.Collator 的创建有成本。大量重复排序时应在外部创建一次并复用,不要在比较函数每次调用时 new。国际化规则也会受 localeoptions 影响,搜索排序、字典排序和文件名排序未必使用同一套配置。

// 错误示例:每次比较都创建 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 会在 nullundefined 上报错。先用空字符串兜底只是最简单规则:

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.Collatorlocaleoptions,而不要依赖系统默认区域设置。

在提交前再保存一份乱序输入和期望数组,让测试直接比较完整顺序;这比人工扫一眼页面更容易发现相等元素、降序和空值规则被后续改坏。

JavaScript 数组排序比较器选择决策树

总结

JavaScript 数组排序的坑主要来自默认字符串比较和原地修改。数字提供减法比较器,文本使用 localeCompareIntl.Collator,对象把规则拆成主次字段,不修改原数组则使用 toSorted 或复制后 sort

写完比较器后不要只看最终列表,还要保留原数组、空值和相等元素三组样例。排序规则明确、输入类型一致、引用边界可控,结果才适合进入界面状态和业务报表。

延伸学习

  1. 通过 JavaScript 原生模块课程 把排序函数拆成可测试模块;
  2. 对照 表格排序实现笔记 理解排序结果如何进入页面;
  3. 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 只有 01,会破坏反向关系并产生不可预测顺序。

Q:对象数组多字段排序怎么写最清楚?

A:先比较主字段,结果不为 0 就直接返回;相等时再比较第二字段。复杂规则拆成具名比较器,比嵌套三元表达式更容易测试。

0 人点赞