JavaScript 数组是现代 Web 开发中数据操作的支柱。随着 ES2023 规范引入了长期变异方法的不可变替代方案,现在是重新审视我们如何使用数组的最佳时机。本文涵盖了数组方法的全部内容,从熟悉的 map、filter 和 reduce 到前沿的添加,并提供了实际示例和性能见解。
数组方法景观
数组方法分为两大类:变异和非变异。 push、pop、splice、sort 和 reverse 等方法会就地修改数组,而 map、filter、slice 和 ES2023 添加的方法会创建新数组。行业趋势正在转向不变性,这可以减少由意外副作用引起的错误。
ES2023 引入了四种解决常见痛点的不可变方法:
const arr = [3, 1, 4, 1, 5, 9];
const sorted = arr.toSorted(); // [1, 1, 3, 4, 5, 9] — new array
const reversed = arr.toReversed(); // [9, 5, 1, 4, 1, 3] — new array
const spliced = arr.toSpliced(1, 2); // [3, 1, 5, 9] — new array
const updated = arr.with(2, 42); // [3, 1, 42, 1, 5, 9] — new array
console.log(arr); // [3, 1, 4, 1, 5, 9] — unmodified
这些方法使得在 React 或 Redux 等状态管理上下文中使用数组更加安全,其中不变性是核心原则。
深度映射、过滤和缩减
函数数组方法的三巨头 — map、filter 和 reduce — 无需显式循环即可实现富有表现力的数据转换。
map 通过回调函数转换每个元素,生成一个相同长度的新数组:
const numbers = [1, 2, 3, 4, 5];
const squared = numbers.map(n => n * n);
// [1, 4, 9, 16, 25]
filter 选择通过谓词测试的元素:
const even = numbers.filter(n => n % 2 === 0);
// [2, 4]
reduce 是最通用的——它可以实现任何其他方法:
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15
// map implemented with reduce
const squared2 = numbers.reduce((acc, n) => [...acc, n * n], []);
一个常见的反模式是当单个 flatMap 就足够时,链接 map 后跟 filter 。每个链接方法都会创建一个中间数组,这对于大型数据集很重要。
| 方法 | 返回 | 变异原创 | 使用案例 |
|---|---|---|---|
| 占位符_0 | 新阵列 | 没有 | 改变每个元素 |
| 占位符_0 | 新阵列 | 没有 | 选择元素子集 |
| 占位符_0 | 任意值 | 没有 | 聚合为单个值 |
| 占位符_0 | 相同的数组 | 是的 | 现场订购 |
| 占位符_0 | 新阵列 | 没有 | 不可变的排序 |
平面映射和分组依据
flatMap 将映射和展平合并到一个通道中。当每个输入元素映射到零个、一个或多个输出元素时,它特别有用:
const sentences = ["hello world", "foo bar"];
const words = sentences.flatMap(s => s.split(" "));
// ["hello", "world", "foo", "bar"]
Object.groupBy (ES2024) 提供本机分组支持:
const users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Charlie", role: "user" },
];
const grouped = Object.groupBy(users, user => user.role);
// { admin: [{ name: "Alice", role: "admin" }],
// user: [{ name: "Bob", ... }, { name: "Charlie", ... }] }
这取代了以前必需的基于 reduce 的手动分组模式。
功能链模式
功能链将操作组成可读的管道:
const result = data
.filter(item => item.isActive)
.map(item => ({ ...item, score: item.value * 2 }))
.filter(item => item.score > 10)
.reduce((acc, item) => acc + item.score, 0);
链中的每个方法都会创建一个中间数组。对于少于 10,000 个元素的数组,此开销可以忽略不计。除此之外,考虑使用单个 reduce 通道或转换器库(如 Ramda)来消除中间分配。
性能考虑因素
简单操作的循环性能通常遵循以下顺序(最快的优先):for 循环、for...of、forEach、map、filter、reduce。然而,可读性应该是默认选择——只有在分析发现瓶颈时才进行优化。
find 方法受益于提前终止,并且可以比 filter 快几个数量级,然后进行索引访问:
const found = arr.find(x => x.id === targetId); // O(n) average, stops early
const found2 = arr.filter(x => x.id === targetId)[0]; // O(n) always, full traversal
现实世界的食谱
使用 Set 可以优雅地处理删除重复项:
const unique = [...new Set([1, 2, 2, 3, 3, 4])];
// [1, 2, 3, 4]
将数组分成组:
const chunk = (arr, size) =>
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);
Fisher-Yates 洗牌提供无偏随机化:
const shuffle = arr => {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
};
TypeScript 集成
正确的键入可以增强数组方法的使用。 filter 中的类型谓词启用类型缩小:
const items: (string | null)[] = ["a", null, "b", null];
const strings: string[] = items.filter((x): x is string => x !== null);
对于复杂的归约,请提供显式的累加器类型:
const grouped = items.reduce<Record<string, number[]>>((acc, item) => {
(acc[item.key] ??= []).push(item.value);
return acc;
}, {});
不变性模式
ES2023 不可变方法(toSorted、toReversed、toSpliced、with)应该成为非性能关键代码的默认方法。它们消除了与突变相关的整个类别的错误,同时保持了可读性。在状态管理中,将它们与扩展运算符结合起来进行复杂的更新:
const state = { items: [1, 2, 3], selected: null };
const next = {
...state,
items: state.items.with(0, 99).toSorted(),
};
掌握数组方法对于编写干净、高效的 JavaScript 至关重要。对于日常任务,从 map、filter 和 reduce 开始,对于特定模式采用 flatMap 和 groupBy,并尽可能默认为 ES2023 不可变方法。

