深入讲解JavaScript中对象数组按指定属性排序及多条件复合排序的实现方案,涵盖数值、中文字符串、日期与空值处理,并提供短路链与通用配置化排序函数。
JS对象数组怎么按指定属性排序?多条件排序实现教程
时间:2026-11-12
浏览:228
文章目录
Array.prototype.sort()(或现代不可变方法 toSorted())。通过向排序方法传入自定义比较函数(comparator),即可轻松按对象的单属性或复合多条件完成升序或降序排列。对于多条件排序,利用逻辑或运算符 || 的短路特性,可以在前置条件相等时依次降级至下一条件,写法简洁且性能高效。
一、核心机制:sort 比较函数的工作契约
JavaScript 的数组实例自带 sort() 方法。当对基本类型(如纯数字或纯字符串)排序时,默认的无参调用会将所有元素转为字符串后按 UTF-16 码元顺序排列。这往往会导致 [10, 2, 5].sort() 得到 [10, 2, 5] 的错误结果。而在处理对象数组时,我们必须提供一个比较函数 compareFunction(a, b)。
比较函数接收数组中的两个对象元素 a 与 b,并根据返回值决定两者的相对次序。ECMAScript 规范对比较函数的返回值有严格定义:
- 返回值小于 0(负数):表示
a的排序权重大于b,a会被排在b之前。 - 返回值大于 0(正数):表示
b的排序权重大于a,b会被排在a之前。 - 返回值等于 0:表示
a与b权重相等,相对位置保持不变(自 ECMAScript 2019 规定为稳定排序)。
Array.prototype.sort() 是一个就地修改(in-place)的方法,调用后会直接变更原数组。如果业务场景需要保留原始数据(如 React 状态管理或不可变架构),请先使用浅拷贝 [...arr].sort(...),或使用现代浏览器及 Node.js 20+ 原生支持的 arr.toSorted(...)。
二、基础实践:单属性排序的四种常见数据类型
在进入多条件排序之前,必须牢固掌握单属性排序的底层差异。不同属性的数据类型(数值、字符串、日期、含空值字段)有其专属的比较逻辑。
1. 数值类型排序
数值是最简单的排序属性。由于两数相减的结果自然满足小于 0、大于 0 或等于 0 的规范:
- 升序(小到大):
a.value - b.value - 降序(大到小):
b.value - a.value
const products = [
{ id: 1, name: '键盘', price: 299 },
{ id: 2, name: '鼠标', price: 99 },
{ id: 3, name: '显示器', price: 1299 }
];
// 按价格升序排列
const sortedByPriceAsc = [...products].sort((a, b) => a.price - b.price);
// 按价格降序排列
const sortedByPriceDesc = [...products].sort((a, b) => b.price - a.price);
2. 字符串类型排序(支持中文与多语言)
许多初学者尝试用 a.name - b.name 对字符串排序,但两个字符串相减会得到 NaN,导致排序失效;若使用大于号比较(如 a.name > b.name ? 1 : -1),则无法按照人类语言规则(如拼音、发音音调)正确排序汉字。
正确的做法是调用字符串原生的 localeCompare() 方法。该方法会根据指定的语言区域(如 'zh-CN')返回 -1、1 或 0,原生支持拼音升降序:
const employees = [
{ id: 1, name: '张伟' },
{ id: 2, name: '白敬亭' },
{ id: 3, name: '陈洁' }
];
// 拼音升序(A - Z)
const sortedByName = [...employees].sort((a, b) =>
a.name.localeCompare(b.name, 'zh-CN')
);
// 拼音降序(Z - A)
const sortedByNameDesc = [...employees].sort((a, b) =>
b.name.localeCompare(a.name, 'zh-CN')
);
3. 日期与时间字符串排序
若属性值为 Date 实例或 ISO 8601 标准日期字符串(例如 '2026-03-15T08:00:00Z'),应先将其转换为毫秒时间戳(getTime() 或 Number(new Date(...))),再执行数值相减:
const articles = [
{ title: 'A', publishAt: '2026-05-12' },
{ title: 'B', publishAt: '2026-01-20' },
{ title: 'C', publishAt: '2026-09-01' }
];
// 按发布时间降序(最新在前)
const sortedByDate = [...articles].sort((a, b) =>
new Date(b.publishAt).getTime() - new Date(a.publishAt).getTime()
);
4. 含有 null、undefined 或空值的边界排序
实际业务数据中经常出现缺失字段或值为 null / undefined 的情况。如果不加处理,null - number 或 undefined.localeCompare 将直接引发运行时异常或破坏排序。通常做法是将空值统一定义为“置底”或“置顶”:
// 将 null / undefined 统一排在最后(置底)
function compareNullable(a, b) {
if (a == null && b == null) return 0;
if (a == null) return 1; // a 为空,移到后面
if (b == null) return -1; // b 为空,移到后面
return a - b;
}
三、核心实现:多条件排序与逻辑或短路语法
多条件排序(又称复合键排序)的核心诉求是:先按第一条件排;如果第一条件分不出高下(结果为 0),再按第二条件排;以此类推。
1. 简洁短路写法(|| 运算符)
在 JavaScript 中,数值 0 在布尔上下文中被视为假值(falsy),而任何非零数值(正数或负数)都被视为真值(truthy)。逻辑或运算符 A || B 的规则是:如果 A 计算结果为真值,则立即返回 A(短路,不再计算 B);如果 A 为假值(即等于 0),则继续计算并返回 B。
这一语言特性与多条件排序的分层判定逻辑完美契合:
const students = [
{ name: '小李', class: '一班', score: 88, age: 15 },
{ name: '小张', class: '二班', score: 95, age: 14 },
{ name: '小王', class: '一班', score: 95, age: 16 },
{ name: '小赵', class: '一班', score: 95, age: 15 }
];
// 排序规则:
// 1. 班级拼音升序
// 2. 得分降序(分数高的在前)
// 3. 年龄升序(分数相同时年龄小的在前)
const sortedStudents = [...students].sort((a, b) => {
return (
a.class.localeCompare(b.class, 'zh-CN') || // 第一优先级
b.score - a.score || // 第二优先级
a.age - b.age // 第三优先级
);
});
四、进阶封装:构建可配置的通用多条件排序工具
在大型前端系统或中后台表格组件(如 Ant Design 或 Element Plus)中,用户经常在界面上动态勾选排序列。如果每次都硬编码 || 链式逻辑,代码将难以维护。
我们可以封装一个声明式的通用多条件排序函数 orderBy(),接收一组规则配置对象,支持动态设置字段名与升降序方向:
/**
* 通用多属性排序器
* @param {Array<Object>} list 原始数组
* @param {Array<{ key: string, order?: 'asc' | 'desc', type?: 'number' | 'string' | 'date' }>} rules 排序规则列表
* @returns {Array<Object>} 排序后的新数组
*/
function orderBy(list, rules) {
// 浅拷贝避免污染入参
return [...list].sort((a, b) => {
for (const rule of rules) {
const { key, order = 'asc', type } = rule;
const valA = a[key];
const valB = b[key];
// 1. 空值防御判定(null / undefined 置底)
if (valA == null && valB == null) continue;
if (valA == null) return 1;
if (valB == null) return -1;
let comparison = 0;
// 2. 根据类型或自动推导比较
const inferredType = type || typeof valA;
if (inferredType === 'number') {
comparison = valA - valB;
} else if (inferredType === 'date') {
comparison = new Date(valA).getTime() - new Date(valB).getTime();
} else {
// 默认为字符串本地化比对
comparison = String(valA).localeCompare(String(valB), 'zh-CN');
}
// 3. 若当前维度分出高下,结合方向立即返回
if (comparison !== 0) {
return order === 'desc' ? -comparison : comparison;
}
}
// 所有条件均相同
return 0;
});
}
// 实际使用示例:
const data = [
{ group: 'B', priority: 1, createdAt: '2026-03-01' },
{ group: 'A', priority: 3, createdAt: '2026-02-15' },
{ group: 'A', priority: 1, createdAt: '2026-04-10' },
{ group: 'A', priority: 1, createdAt: '2026-01-05' }
];
const result = orderBy(data, [
{ key: 'group', order: 'asc' }, // 先按分组字母升序
{ key: 'priority', order: 'desc' }, // 再按优先级降序
{ key: 'createdAt', order: 'asc', type: 'date' } // 最后按日期升序
]);
console.log(result);
五、生产环境高频避坑与性能优化
在实际编写排序逻辑时,有一些容易被忽视的隐蔽陷阱,这些错误常常会导致排序行为异常甚至生产故障:
施瓦茨变换(Schwartzian Transform / Map-Sort-Map)在大数据量下的应用
当待排序的数据达到数万条,且排序依据是需要较重计算的派生值(例如复杂的日期解析、权重公式或拼音提取)时,比较器内部如果反复执行解析,会导致数万次重复计算。通过施瓦茨变换,只需预先计算一次(时间复杂度 O(N)):
// 施瓦茨变换优化示例:预先提取时间戳与比较键
function sortByComplexDerivedKey(list) {
return list
// 1. 映射(Decorate):预计算开销较大的字段
.map((item, index) => ({
index,
item,
computedTimestamp: new Date(item.dateString).getTime(),
computedWeight: item.level === 'VIP' ? 100 : 10
}))
// 2. 排序(Sort):比较器仅访问预计算出的轻量属性
.sort((a, b) =>
b.computedWeight - a.computedWeight ||
b.computedTimestamp - a.computedTimestamp ||
a.index - b.index // 显式保证稳定性
)
// 3. 还原(Undecorate):提取回原始对象
.map(wrapper => wrapper.item);
}
六、总结与速查对照表
掌握 JavaScript 对象数组排序的核心在于理清比较函数返回负、零、正数的契约,以及通过逻辑或短路 || 实现分层穿透。在不同业务类型场景下,建议参考以下推荐实现:
| 场景需求 | 比较代码模版 | 关键注意事项 |
|---|---|---|
| 单数值属性升序 | (a, b) => a.num - b.num |
防范极端溢出或非数值 NaN |
| 单数值属性降序 | (a, b) => b.num - a.num |
降序即交换两项次序 |
| 中文字符串拼音升序 | (a, b) => a.str.localeCompare(b.str, 'zh-CN') |
禁止使用减号或仅凭 > 判断 |
| 多条件复合排序 | (a, b) => a.group.localeCompare(b.group) || b.score - a.score || a.id - b.id |
利用 0 的假值特性实现逐级穿透 |
| 含空值安全置底 | (a, b) => (a.val == null) - (b.val == null) || a.val - b.val |
优先过滤或显式判断 null / undefined |
在编写现代前端业务逻辑时,优先推荐将排序写为纯函数形式,搭配 TypeScript 的类型定义与声明式排序规则,能显著降低数据处理逻辑的维护负担。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。