Skip to content

数组排序方法:让数据井然有序

走进一家整洁的书店,你会发现书籍不是随意摆放的。小说按作者姓氏字母顺序排列,技术书籍按主题分类,杂志按出版日期从新到旧。这种有序的组织让顾客能快速找到想要的书。在编程世界中,排序扮演着相似的角色——它帮助我们将杂乱无章的数据整理成有意义的顺序,让信息更容易被理解和处理。

sort() - 数组排序的核心方法

sort() 方法是 JavaScript 中最常用的排序工具。它会对数组元素进行原地排序(in-place),也就是说会直接修改原数组,而不是创建新数组。

默认排序行为

默认情况下,sort() 会将所有元素转换为字符串,然后按照 Unicode 码点顺序进行比较:

javascript
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"]

对于字符串数组,默认排序通常符合我们的期望。但对于数字,情况就不同了:

javascript
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"。

自定义比较函数

要正确排序数字或实现自定义排序逻辑,我们需要提供比较函数。比较函数接收两个参数 ab,返回一个数字:

  • 如果返回值 < 0,a 会被排在 b 之前
  • 如果返回值 = 0,ab 的相对位置不变
  • 如果返回值 > 0,a 会被排在 b 之后
javascript
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 排在后面(升序)

对象数组排序

排序对象数组是实际开发中非常常见的需求:

javascript
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() 方法,它能正确处理各种语言和特殊字符:

javascript
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"));

多字段排序

有时我们需要基于多个条件进行排序,比如先按部门分组,再按工资排序:

javascript
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),它们在排序后的相对位置会保持不变:

javascript
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" }   // 保持原顺序
// ]

实际应用:电商产品排序

javascript
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() 方法会反转数组中元素的顺序,同样是原地操作,会修改原数组:

javascript
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() 结合使用。虽然我们可以通过自定义比较函数实现降序排序,但有时直接反转已排序的数组更简洁:

javascript
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"]

实际应用:显示最新消息

javascript
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 morning

toSorted() - 非变异排序

ES2023 引入了 toSorted() 方法,它与 sort() 功能相同,但不会修改原数组,而是返回一个新的排序后的数组:

javascript
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() 更清晰:

javascript
// 旧方法:手动复制
let numbers = [5, 2, 8, 1, 9];
let sortedOld = [...numbers].sort((a, b) => a - b);

// 新方法:使用 toSorted()
let sortedNew = numbers.toSorted((a, b) => a - b);

实际应用:多视图数据展示

javascript
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() 方法,它返回反转后的新数组,不修改原数组:

javascript
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() 可以优雅地链式调用,因为它们都返回新数组:

javascript
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. 自然排序(版本号、文件名)

对于包含数字的字符串,我们可能希望按数字大小而非字符顺序排序:

javascript
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. 忽略大小写排序

javascript
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. 日期排序

javascript
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 值排在前面或后面:

javascript
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. 空值处理

处理可能包含 nullundefined 的数组:

javascript
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):

javascript
// 对小数组排序很快
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

优化比较函数

比较函数会被调用很多次,优化它能显著提升性能:

javascript
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);
}

避免不必要的排序

javascript
// ❌ 不必要:多次排序
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);
}

实战案例:数据表格排序

构建一个支持多列排序的数据表格:

javascript
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() 会修改原数组

javascript
// ❌ 意外修改了原数组
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. 比较函数必须返回一致的结果

javascript
// ❌ 错误:不一致的比较函数
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. 数字排序忘记提供比较函数

javascript
// ❌ 错误:数字被当作字符串排序
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. 链式调用时注意返回值

javascript
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() - 非变异反转,返回新数组,保持原数组不变

掌握这些方法的关键在于:

  1. 理解默认排序行为(字符串比较)
  2. 学会编写高效的比较函数
  3. 知道何时使用变异方法,何时使用非变异方法
  4. 处理特殊情况(null、日期、多字段排序等)
  5. 注意性能优化,避免不必要的排序操作

排序是数据处理的基础操作,无论是展示用户列表、商品排名,还是数据分析,都离不开排序。掌握这些排序方法,你就能让数据按照最有意义的方式呈现给用户。