数组排序方法:让数据井然有序
走进一家整洁的书店,你会发现书籍不是随意摆放的。小说按作者姓氏字母顺序排列,技术书籍按主题分类,杂志按出版日期从新到旧。这种有序的组织让顾客能快速找到想要的书。在编程世界中,排序扮演着相似的角色——它帮助我们将杂乱无章的数据整理成有意义的顺序,让信息更容易被理解和处理。
sort() - 数组排序的核心方法
sort() 方法是 JavaScript 中最常用的排序工具。它会对数组元素进行原地排序(in-place),也就是说会直接修改原数组,而不是创建新数组。
默认排序行为
默认情况下,sort() 会将所有元素转换为字符串,然后按照 Unicode 码点顺序进行比较:
let fruits = ["banana", "apple", "cherry", "date"];
fruits.sort();
console.log(fruits); // ["apple", "banana", "cherry", "date"]
let letters = ["z", "a", "m", "b"];
letters.sort();
console.log(letters); // ["a", "b", "m", "z"]对于字符串数组,默认排序通常符合我们的期望。但对于数字,情况就不同了:
let numbers = [10, 5, 40, 25, 1000, 1];
numbers.sort();
console.log(numbers); // [1, 10, 1000, 25, 40, 5]结果可能出乎意料!为什么 1000 排在 25 前面,5 排在最后?这是因为 sort() 默认将数字转换为字符串进行比较:"1000" < "25" 因为字符 "1" 的 Unicode 码点小于 "2"。
自定义比较函数
要正确排序数字或实现自定义排序逻辑,我们需要提供比较函数。比较函数接收两个参数 a 和 b,返回一个数字:
- 如果返回值 < 0,
a会被排在b之前 - 如果返回值 = 0,
a和b的相对位置不变 - 如果返回值 > 0,
a会被排在b之后
let numbers = [10, 5, 40, 25, 1000, 1];
// 升序排序
numbers.sort((a, b) => a - b);
console.log(numbers); // [1, 5, 10, 25, 40, 1000]
// 降序排序
numbers.sort((a, b) => b - a);
console.log(numbers); // [1000, 40, 25, 10, 5, 1]这个简单的 a - b 模式是数字排序的标准写法:
- 当
a < b时,a - b为负数,a排在前面(升序) - 当
a > b时,a - b为正数,a排在后面(升序)
对象数组排序
排序对象数组是实际开发中非常常见的需求:
let students = [
{ name: "Alice", score: 85 },
{ name: "Bob", score: 92 },
{ name: "Charlie", score: 78 },
{ name: "David", score: 95 },
{ name: "Emma", score: 88 },
];
// 按分数升序排序
students.sort((a, b) => a.score - b.score);
console.log(students);
// [
// { name: "Charlie", score: 78 },
// { name: "Alice", score: 85 },
// { name: "Emma", score: 88 },
// { name: "Bob", score: 92 },
// { name: "David", score: 95 }
// ]
// 按分数降序排序(找前三名)
students.sort((a, b) => b.score - a.score);
console.log(students.slice(0, 3));
// [
// { name: "David", score: 95 },
// { name: "Bob", score: 92 },
// { name: "Emma", score: 88 }
// ]对于字符串字段的排序,我们使用 localeCompare() 方法,它能正确处理各种语言和特殊字符:
let users = [
{ name: "Zhang Wei", age: 28 },
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "李明", age: 27 },
];
// 按名字字母顺序排序
users.sort((a, b) => a.name.localeCompare(b.name));
console.log(users.map((u) => u.name));
// ["Alice", "Bob", "李明", "Zhang Wei"]
// 使用中文语言环境排序
users.sort((a, b) => a.name.localeCompare(b.name, "zh-CN"));多字段排序
有时我们需要基于多个条件进行排序,比如先按部门分组,再按工资排序:
let employees = [
{ name: "Alice", department: "Engineering", salary: 90000 },
{ name: "Bob", department: "Sales", salary: 75000 },
{ name: "Charlie", department: "Engineering", salary: 95000 },
{ name: "David", department: "Sales", salary: 80000 },
{ name: "Emma", department: "Engineering", salary: 85000 },
];
// 先按部门排序,部门相同则按工资降序排序
employees.sort((a, b) => {
// 首先比较部门
let deptCompare = a.department.localeCompare(b.department);
if (deptCompare !== 0) {
return deptCompare;
}
// 部门相同,比较工资(降序)
return b.salary - a.salary;
});
console.log(employees);
// [
// { name: "Charlie", department: "Engineering", salary: 95000 },
// { name: "Alice", department: "Engineering", salary: 90000 },
// { name: "Emma", department: "Engineering", salary: 85000 },
// { name: "David", department: "Sales", salary: 80000 },
// { name: "Bob", department: "Sales", salary: 75000 }
// ]稳定排序
从 ES2019 开始,JavaScript 的 sort() 方法保证是稳定排序。这意味着如果两个元素被认为相等(比较函数返回 0),它们在排序后的相对位置会保持不变:
let records = [
{ id: 1, priority: 2, timestamp: "10:00" },
{ id: 2, priority: 1, timestamp: "10:05" },
{ id: 3, priority: 2, timestamp: "10:10" },
{ id: 4, priority: 1, timestamp: "10:15" },
];
// 按优先级排序
records.sort((a, b) => a.priority - b.priority);
console.log(records);
// priority 相同的元素保持原来的顺序
// [
// { id: 2, priority: 1, timestamp: "10:05" },
// { id: 4, priority: 1, timestamp: "10:15" }, // 保持原顺序
// { id: 1, priority: 2, timestamp: "10:00" },
// { id: 3, priority: 2, timestamp: "10:10" } // 保持原顺序
// ]实际应用:电商产品排序
function sortProducts(products, sortBy) {
let sorted = [...products]; // 创建副本避免修改原数组
switch (sortBy) {
case "price-low":
return sorted.sort((a, b) => a.price - b.price);
case "price-high":
return sorted.sort((a, b) => b.price - a.price);
case "rating":
return sorted.sort((a, b) => {
// 先按评分降序
let ratingDiff = b.rating - a.rating;
if (ratingDiff !== 0) return ratingDiff;
// 评分相同则按评论数降序
return b.reviews - a.reviews;
});
case "popularity":
return sorted.sort((a, b) => b.sales - a.sales);
case "newest":
return sorted.sort(
(a, b) => new Date(b.releaseDate) - new Date(a.releaseDate)
);
default:
return sorted;
}
}
let products = [
{
name: "Laptop",
price: 999,
rating: 4.5,
reviews: 234,
sales: 1200,
releaseDate: "2024-01-15",
},
{
name: "Mouse",
price: 25,
rating: 4.8,
reviews: 567,
sales: 3400,
releaseDate: "2024-02-01",
},
{
name: "Keyboard",
price: 75,
rating: 4.5,
reviews: 189,
sales: 890,
releaseDate: "2024-01-20",
},
];
console.log("By price (low to high):");
sortProducts(products, "price-low").forEach((p) =>
console.log(`${p.name}: $${p.price}`)
);
// Mouse: $25
// Keyboard: $75
// Laptop: $999
console.log("\nBy rating:");
sortProducts(products, "rating").forEach((p) =>
console.log(`${p.name}: ${p.rating} (${p.reviews} reviews)`)
);
// Mouse: 4.8 (567 reviews)
// Laptop: 4.5 (234 reviews)
// Keyboard: 4.5 (189 reviews)reverse() - 反转数组顺序
reverse() 方法会反转数组中元素的顺序,同样是原地操作,会修改原数组:
let numbers = [1, 2, 3, 4, 5];
numbers.reverse();
console.log(numbers); // [5, 4, 3, 2, 1]
let fruits = ["apple", "banana", "cherry"];
fruits.reverse();
console.log(fruits); // ["cherry", "banana", "apple"]reverse() 常常与 sort() 结合使用。虽然我们可以通过自定义比较函数实现降序排序,但有时直接反转已排序的数组更简洁:
let words = ["zebra", "apple", "mango", "banana"];
// 方法 1:使用比较函数降序排序
let desc1 = [...words].sort((a, b) => b.localeCompare(a));
console.log(desc1); // ["zebra", "mango", "banana", "apple"]
// 方法 2:先升序排序,再反转
let desc2 = [...words].sort().reverse();
console.log(desc2); // ["zebra", "mango", "banana", "apple"]实际应用:显示最新消息
let messages = [
{ id: 1, text: "Hello", timestamp: "2024-01-01 10:00" },
{ id: 2, text: "How are you?", timestamp: "2024-01-01 10:05" },
{ id: 3, text: "Good morning", timestamp: "2024-01-01 09:55" },
{ id: 4, text: "See you later", timestamp: "2024-01-01 10:10" },
];
// 按时间排序后反转,显示最新的消息在前
messages
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
.reverse();
console.log("Latest messages:");
messages.forEach((msg) => {
console.log(`[${msg.timestamp}] ${msg.text}`);
});
// [2024-01-01 10:10] See you later
// [2024-01-01 10:05] How are you?
// [2024-01-01 10:00] Hello
// [2024-01-01 09:55] Good morningtoSorted() - 非变异排序
ES2023 引入了 toSorted() 方法,它与 sort() 功能相同,但不会修改原数组,而是返回一个新的排序后的数组:
let original = [3, 1, 4, 1, 5, 9, 2, 6];
// 使用 toSorted() - 原数组不变
let sorted = original.toSorted((a, b) => a - b);
console.log(sorted); // [1, 1, 2, 3, 4, 5, 6, 9]
console.log(original); // [3, 1, 4, 1, 5, 9, 2, 6] - 未被修改
// 对比 sort() - 原数组被修改
let original2 = [3, 1, 4, 1, 5, 9, 2, 6];
let sorted2 = original2.sort((a, b) => a - b);
console.log(sorted2); // [1, 1, 2, 3, 4, 5, 6, 9]
console.log(original2); // [1, 1, 2, 3, 4, 5, 6, 9] - 被修改了toSorted() 在函数式编程风格中特别有用,因为它不会产生副作用。当你需要保留原数组不变时,使用 toSorted() 比先复制再 sort() 更清晰:
// 旧方法:手动复制
let numbers = [5, 2, 8, 1, 9];
let sortedOld = [...numbers].sort((a, b) => a - b);
// 新方法:使用 toSorted()
let sortedNew = numbers.toSorted((a, b) => a - b);实际应用:多视图数据展示
class DataView {
constructor(data) {
this.originalData = data;
}
// 显示原始顺序
showOriginal() {
return this.originalData;
}
// 按价格显示
showByPrice() {
return this.originalData.toSorted((a, b) => a.price - b.price);
}
// 按名称显示
showByName() {
return this.originalData.toSorted((a, b) => a.name.localeCompare(b.name));
}
// 按库存显示
showByStock() {
return this.originalData.toSorted((a, b) => b.stock - a.stock);
}
}
let products = [
{ name: "Laptop", price: 999, stock: 5 },
{ name: "Mouse", price: 25, stock: 50 },
{ name: "Keyboard", price: 75, stock: 30 },
];
let view = new DataView(products);
console.log("Original order:", view.showOriginal());
console.log("By price:", view.showByPrice());
console.log("By name:", view.showByName());
console.log("Original unchanged:", view.showOriginal());
// 原数组始终保持不变toReversed() - 非变异反转
与 toSorted() 类似,ES2023 也引入了 toReversed() 方法,它返回反转后的新数组,不修改原数组:
let numbers = [1, 2, 3, 4, 5];
// 使用 toReversed() - 原数组不变
let reversed = numbers.toReversed();
console.log(reversed); // [5, 4, 3, 2, 1]
console.log(numbers); // [1, 2, 3, 4, 5] - 未被修改
// 对比 reverse() - 原数组被修改
let numbers2 = [1, 2, 3, 4, 5];
let reversed2 = numbers2.reverse();
console.log(reversed2); // [5, 4, 3, 2, 1]
console.log(numbers2); // [5, 4, 3, 2, 1] - 被修改了链式调用
toSorted() 和 toReversed() 可以优雅地链式调用,因为它们都返回新数组:
let scores = [85, 92, 78, 95, 88];
// 排序后反转(降序)
let descending = scores.toSorted((a, b) => a - b).toReversed();
console.log(descending); // [95, 92, 88, 85, 78]
console.log(scores); // [85, 92, 78, 95, 88] - 原数组不变
// 结合其他数组方法
let topThree = scores
.toSorted((a, b) => b - a)
.slice(0, 3)
.map((score) => `Score: ${score}`);
console.log(topThree); // ["Score: 95", "Score: 92", "Score: 88"]高级排序技巧
1. 自然排序(版本号、文件名)
对于包含数字的字符串,我们可能希望按数字大小而非字符顺序排序:
let versions = ["v1.10.0", "v1.2.0", "v1.1.0", "v2.0.0", "v1.9.0"];
// 错误:字符串排序
console.log([...versions].sort());
// ["v1.1.0", "v1.10.0", "v1.2.0", "v1.9.0", "v2.0.0"]
// 1.10 被排在 1.2 前面
// 正确:自然排序
versions.sort((a, b) =>
a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })
);
console.log(versions);
// ["v1.1.0", "v1.2.0", "v1.9.0", "v1.10.0", "v2.0.0"]localeCompare() 的 numeric: true 选项会将字符串中的数字序列作为数字比较。
2. 忽略大小写排序
let names = ["alice", "Bob", "Charlie", "david", "Emma"];
// 默认排序:大写字母在前
console.log([...names].sort());
// ["Bob", "Charlie", "Emma", "alice", "david"]
// 忽略大小写排序
names.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
console.log(names);
// ["alice", "Bob", "Charlie", "david", "Emma"]
// 或使用 localeCompare 选项
names.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));3. 日期排序
let events = [
{ name: "Meeting", date: "2024-03-15" },
{ name: "Conference", date: "2024-01-20" },
{ name: "Workshop", date: "2024-02-10" },
{ name: "Seminar", date: "2024-03-01" },
];
// 按日期升序排序
events.sort((a, b) => new Date(a.date) - new Date(b.date));
console.log(events.map((e) => `${e.name}: ${e.date}`));
// [
// "Conference: 2024-01-20",
// "Workshop: 2024-02-10",
// "Seminar: 2024-03-01",
// "Meeting: 2024-03-15"
// ]
// 找出最近的事件
let nextEvent = events.toSorted(
(a, b) => new Date(a.date) - new Date(b.date)
)[0];
console.log(`Next event: ${nextEvent.name} on ${nextEvent.date}`);4. 布尔值排序
将 true 值排在前面或后面:
let tasks = [
{ task: "Buy groceries", completed: false },
{ task: "Finish report", completed: true },
{ task: "Call dentist", completed: false },
{ task: "Exercise", completed: true },
];
// 已完成的排在前面
tasks.sort((a, b) => b.completed - a.completed);
console.log(tasks);
// [
// { task: "Finish report", completed: true },
// { task: "Exercise", completed: true },
// { task: "Buy groceries", completed: false },
// { task: "Call dentist", completed: false }
// ]
// 或使用更直观的写法
tasks.sort((a, b) => {
if (a.completed === b.completed) return 0;
return a.completed ? -1 : 1; // true 排在前面
});5. 空值处理
处理可能包含 null 或 undefined 的数组:
let values = [5, null, 3, undefined, 8, null, 1];
// 将 null 和 undefined 排在最后
values.sort((a, b) => {
if (a == null && b == null) return 0;
if (a == null) return 1; // a 是 null,排在后面
if (b == null) return -1; // b 是 null,排在后面
return a - b; // 都不是 null,正常比较
});
console.log(values); // [1, 3, 5, 8, null, undefined, null]性能考虑
排序算法复杂度
JavaScript 引擎通常使用高效的排序算法(如 Timsort),时间复杂度一般为 O(n log n):
// 对小数组排序很快
let small = Array.from({ length: 100 }, () => Math.random());
console.time("small");
small.sort((a, b) => a - b);
console.timeEnd("small"); // 通常 < 1ms
// 大数组也能快速排序
let large = Array.from({ length: 100000 }, () => Math.random());
console.time("large");
large.sort((a, b) => a - b);
console.timeEnd("large"); // 通常 10-50ms优化比较函数
比较函数会被调用很多次,优化它能显著提升性能:
let products = [
/* 大量产品数据 */
];
// ❌ 低效:每次比较都创建 Date 对象
products.sort((a, b) => new Date(a.date) - new Date(b.date));
// ✅ 高效:预先转换日期
products.forEach((p) => {
p._dateValue = new Date(p.date).getTime();
});
products.sort((a, b) => a._dateValue - b._dateValue);
// 或者缓存转换结果
function optimizedDateSort(array, dateField) {
return array
.map((item, index) => ({
item,
date: new Date(item[dateField]).getTime(),
index,
}))
.sort((a, b) => a.date - b.date || a.index - b.index)
.map((entry) => entry.item);
}避免不必要的排序
// ❌ 不必要:多次排序
function getTopItems(items, count) {
let sorted = items.sort((a, b) => b.score - a.score);
return sorted.slice(0, count);
}
// ✅ 优化:只需要前 N 项时,可以用其他方法
function getTopItemsOptimized(items, count) {
// 对于小的 count,部分排序更高效
if (count < items.length / 10) {
let result = [];
let remaining = [...items];
for (let i = 0; i < count; i++) {
let maxIndex = 0;
for (let j = 1; j < remaining.length; j++) {
if (remaining[j].score > remaining[maxIndex].score) {
maxIndex = j;
}
}
result.push(remaining[maxIndex]);
remaining.splice(maxIndex, 1);
}
return result;
}
return items.toSorted((a, b) => b.score - a.score).slice(0, count);
}实战案例:数据表格排序
构建一个支持多列排序的数据表格:
class SortableTable {
constructor(data) {
this.data = data;
this.sortConfig = { column: null, direction: "asc" };
}
sortBy(column, direction = "asc") {
this.sortConfig = { column, direction };
this.data.sort((a, b) => {
let aValue = a[column];
let bValue = b[column];
// 处理不同类型
if (typeof aValue === "string" && typeof bValue === "string") {
// 字符串比较
let comparison = aValue.localeCompare(bValue);
return direction === "asc" ? comparison : -comparison;
} else if (typeof aValue === "number" && typeof bValue === "number") {
// 数字比较
return direction === "asc" ? aValue - bValue : bValue - aValue;
} else if (aValue instanceof Date && bValue instanceof Date) {
// 日期比较
return direction === "asc" ? aValue - bValue : bValue - aValue;
}
return 0;
});
return this;
}
toggleSort(column) {
if (this.sortConfig.column === column) {
// 切换方向
let newDirection = this.sortConfig.direction === "asc" ? "desc" : "asc";
return this.sortBy(column, newDirection);
} else {
// 新列,默认升序
return this.sortBy(column, "asc");
}
}
getSortedData() {
return this.data;
}
getSortIndicator(column) {
if (this.sortConfig.column !== column) {
return "";
}
return this.sortConfig.direction === "asc" ? " ↑" : " ↓";
}
}
// 使用示例
let employees = [
{
name: "Alice",
department: "Engineering",
salary: 90000,
hireDate: new Date("2020-03-15"),
},
{
name: "Bob",
department: "Sales",
salary: 75000,
hireDate: new Date("2019-07-22"),
},
{
name: "Charlie",
department: "Engineering",
salary: 95000,
hireDate: new Date("2021-01-10"),
},
{
name: "David",
department: "Marketing",
salary: 70000,
hireDate: new Date("2020-09-05"),
},
];
let table = new SortableTable(employees);
// 按薪水升序排序
table.sortBy("salary", "asc");
console.log("Sorted by salary:", table.getSortedData());
// 切换到降序
table.toggleSort("salary");
console.log("Toggle salary:", table.getSortedData());
// 按部门排序
table.sortBy("department");
console.log("Sorted by department:", table.getSortedData());常见陷阱与最佳实践
1. 忘记 sort() 会修改原数组
// ❌ 意外修改了原数组
let original = [3, 1, 4, 1, 5];
let sorted = original.sort((a, b) => a - b);
console.log(original); // [1, 1, 3, 4, 5] - 被修改了!
// ✅ 使用 toSorted() 或先复制
let original2 = [3, 1, 4, 1, 5];
let sorted2 = original2.toSorted((a, b) => a - b);
// 或
let sorted3 = [...original2].sort((a, b) => a - b);
console.log(original2); // [3, 1, 4, 1, 5] - 未被修改2. 比较函数必须返回一致的结果
// ❌ 错误:不一致的比较函数
let data = [1, 2, 3, 4, 5];
data.sort(() => Math.random() - 0.5); // 随机排序?
// 结果不可预测,可能导致无限循环
// ✅ 正确:随机打乱数组使用 Fisher-Yates 算法
function shuffle(array) {
let result = [...array];
for (let i = result.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}3. 数字排序忘记提供比较函数
// ❌ 错误:数字被当作字符串排序
let numbers = [10, 5, 40, 25, 1000, 1];
numbers.sort();
console.log(numbers); // [1, 10, 1000, 25, 40, 5] - 错误!
// ✅ 正确:提供数字比较函数
numbers.sort((a, b) => a - b);
console.log(numbers); // [1, 5, 10, 25, 40, 1000]4. 链式调用时注意返回值
let numbers = [3, 1, 4, 1, 5, 9, 2, 6];
// ✅ sort() 返回排序后的数组(原数组)
let result1 = numbers.sort((a, b) => a - b).slice(0, 3);
console.log(result1); // [1, 1, 2]
console.log(numbers); // [1, 1, 2, 3, 4, 5, 6, 9] - 被修改
// ✅ toSorted() 返回新数组
let numbers2 = [3, 1, 4, 1, 5, 9, 2, 6];
let result2 = numbers2.toSorted((a, b) => a - b).slice(0, 3);
console.log(result2); // [1, 1, 2]
console.log(numbers2); // [3, 1, 4, 1, 5, 9, 2, 6] - 未被修改总结
JavaScript 数组排序方法为我们提供了强大而灵活的数据组织工具:
sort()- 原地排序,接受自定义比较函数,适合直接修改数组的场景reverse()- 原地反转数组顺序,常与sort()配合实现降序toSorted()- 非变异排序,返回新数组,适合函数式编程toReversed()- 非变异反转,返回新数组,保持原数组不变
掌握这些方法的关键在于:
- 理解默认排序行为(字符串比较)
- 学会编写高效的比较函数
- 知道何时使用变异方法,何时使用非变异方法
- 处理特殊情况(null、日期、多字段排序等)
- 注意性能优化,避免不必要的排序操作
排序是数据处理的基础操作,无论是展示用户列表、商品排名,还是数据分析,都离不开排序。掌握这些排序方法,你就能让数据按照最有意义的方式呈现给用户。