在 JavaScript 中处理数组,现在很少需要再手动编写 for 循环了。该语言现在提供了更具表现力的工具,可以将嵌套且杂乱的数据转化为简洁、易读的转换结果。本指南将从基础知识进阶,重点讲解扁平化 (flattening)、归约 (reducing)、查询 (querying) 以及能够保持原始数据不变的新型不可变模式 (immutable patterns)。

使用 flat()flatMap() 扁平化嵌套结构

现实世界中的数据往往是嵌套的。无论是从 API 获取记录、解析 CSV 还是读取配置文件,你经常会遇到数组套数组的情况。flat() 的作用就是将这些层级压缩成一个单一的线性列表。

对嵌套数组调用 flat(),默认会扁平化一层:

const nested = [[1, 2], [3, 4]];
const flat = nested.flat();
// [1, 2, 3, 4]

原始的 nested 数组保持不变。如果嵌套层级更深,可以传递深度参数,例如 flat(2),或者在不知道有多少层时使用 flat(Infinity)。这比手动编写递归辅助函数要高效得多。

当你需要既转换元素又扁平化结果时,flatMap() 可以通过一次遍历同时完成这两项工作。它会对每个元素运行一个映射函数,然后将返回的数组扁平化一层。虽然使用分开的 .map().flat() 调用也可以实现,但 flatMap() 跳过了中间数组,从而节省了内存并减少了第二次迭代。一个经典的用例是将句子数组拆分为单个单词:

const lines = ['hello world', 'foo bar'];
const words = lines.flatMap(line => line.split(' '));
// ['hello', 'world', 'foo', 'bar']

过滤 (Filtering) 与映射 (Mapping):了解其区别

filter() 会创建一个新数组,其中仅包含通过测试的项目。回调函数应返回一个真值 (truthy) 以保留该元素,或返回一个假值 (falsy) 以丢弃它。它绝不会修改源数组。

const users = [
  { name: 'Ajay', active: true },
  { name: 'Bina', active: false }
];
const activeUsers = users.filter(u => u.active);

filter()map() 很容易混淆,因为它们都返回新数组并接受回调函数。区别在于它们的“契约” (contract)。map() 返回一个长度相同的新数组。如果你转换每个元素并运行条件检查,你确实会在每个位置看到 truefalse,因为 map() 从不丢弃项目——它只是改变它们。相比之下,filter() 返回的是一个可能更短的数组,其中包含通过测试的元素。如果你想删除数据,请使用 filter();如果你想在保留元素数量的同时转换每条数据,请使用 map()

使用 reduce() 归约数组

map()filter() 返回的是数组,而 reduce() 则将整个列表归约 (boil down) 为一个值。它从左向右遍历,在每一步中携带一个累加器 (accumulator)。

const totals = [10, 20, 30];
const sum = totals.reduce((acc, current) => acc + current, 0);
// 60

第二个参数(上述示例中的 0)是初始累加器值。如果省略它,JavaScript 会使用第一个元素作为起点,从索引 1 开始迭代。当数组为空时,这会让人们感到困惑,因为在空数组上调用没有初始值的 reduce() 会抛出 TypeError

reduce 不仅仅用于算术运算。你可以用它来构建频率表、合并对象、查找最大值,或者通过多个转换步骤对数据进行管道处理 (pipe data)。由于它非常灵活,因此保持回调函数简洁并清晰地命名变量是非常值得的。一个过于密集的 reduce() 代码块很快就会变得比它所取代的循环更难阅读。

布尔查询:every()some()

有时你不需要一个新数组,而是需要关于集合的一个“是或否”的答案。只有当所有元素都满足你的条件时,every() 才会返回 true。如果第一个元素就不满足条件,它会立即停止并返回 false

const scores = [85, 92, 78];
const allPassed = scores.every(s => s >= 50);

some() 是它的镜像。如果至少有一个元素通过测试,它就会返回 true。一旦找到匹配项,它会立即“短路” (short-circuit) 停止执行。

const hasPerfectScore = scores.some(s => s === 100);

这些方法取代了用于校验检查的手动循环。需要确认每个表单字段都已填写?使用 every()。需要知道是否有任何用户拥有管理员权限?使用 some()

无需修改即可进行检查和更新

现代 JavaScript 非常强调不可变性 (immutability)。这些方法可以帮助你在不触动原始数组的情况下检查或更新数组。

keys() 返回一个包含数组索引的迭代器对象。entries() 返回一个包含 [index, value] 对的迭代器。当你想进行简洁的解构赋值时,它们在 for...of 循环中表现出色:

const fruits = ['apple', 'banana'];
for (const [index, fruit] of fruits.entries()) {
  console.log(index, fruit);
}

with() 是语言中较新的特性,它通过替换特定索引处的元素并返回一个全新的数组。它满足了与方括号赋值(arr[index] = value)相同的需求,不同之处在于它不会修改原数组。在 React 等框架中,直接修改(mutation)会破坏变更检测,因此使用 with() 是更安全的选择:

const original = [10, 20, 30];
const updated = original.with(1, 99);
// original: [10, 20, 30]
// updated:  [10, 99, 30]

展开与剩余:... 的两个方向

展开语法 (spread syntax) 和剩余参数 (rest parameter) 都使用三个点,但它们的数据流动方向截然相反。

使用展开运算符 (spread operator) 将数组展开为单个元素。这是合并或克隆数组最简洁的方式:

const front = [1, 2];
const back = [3, 4];
const merged = [...front, ...back];

const copy = [...merged];

你也可以轻松地插入额外的项:[0, ...front, 99, ...back]

剩余参数 (rest parameter) 将多个元素收集到一个数组中。在函数签名中,它会收集所有剩余的参数:

function logFirst(first, ...rest) {
  console.log('First:', first);
  console.log('Rest:', rest);
}
logFirst('a', 'b', 'c');
// First: a
// Rest: ['b', 'c']

它们共同处理常见的任务:接收变长输入 (variadic input),将其作为数组进行处理,然后将其重新展开到函数调用或新的数据结构中。

核心要点

使用这些方法的目的不是为了用单行代码向同事炫技,而是为了让你的意图显而易见。当其他开发者看到 filter() 时,他们知道你正在移除项;当他们看到 reduce() 时,他们知道你正在聚合数据;当他们看到 with() 时,他们知道原始数组在某处仍然保持完整。精通这些工具意味着编写能够“自解释”的代码,这比任何巧妙的循环都能节省更多的时间。