Featured image of post JavaScript 数组方法:从基础知识到高级模式Featured image of post JavaScript 数组方法:从基础知识到高级模式

JavaScript 数组方法:从基础知识到高级模式

深入研究 JavaScript 数组方法:map、filter、reduce、flatMap、类型化数组、性能基准、链接模式和不变性。

JavaScript 数组是现代 Web 开发中数据操作的支柱。随着 ES2023 规范引入了长期变异方法的不可变替代方案,现在是重新审视我们如何使用数组的最佳时机。本文涵盖了数组方法的全部内容,从熟悉的 mapfilterreduce 到前沿的添加,并提供了实际示例和性能见解。

数组方法景观

数组方法分为两大类:变异和非变异。 pushpopsplicesortreverse 等方法会就地修改数组,而 mapfilterslice 和 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 等状态管理上下文中使用数组更加安全,其中不变性是核心原则。


深度映射、过滤和缩减

函数数组方法的三巨头 — mapfilterreduce — 无需显式循环即可实现富有表现力的数据转换。

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...offorEachmapfilterreduce。然而,可读性应该是默认选择——只有在分析发现瓶颈时才进行优化。

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 不可变方法(toSortedtoReversedtoSplicedwith)应该成为非性能关键代码的默认方法。它们消除了与突变相关的整个类别的错误,同时保持了可读性。在状态管理中,将它们与扩展运算符结合起来进行复杂的更新:

const state = { items: [1, 2, 3], selected: null };
const next = {
  ...state,
  items: state.items.with(0, 99).toSorted(),
};

掌握数组方法对于编写干净、高效的 JavaScript 至关重要。对于日常任务,从 mapfilterreduce 开始,对于特定模式采用 flatMapgroupBy,并尽可能默认为 ES2023 不可变方法。