Menu

JavaScript 数组方法:map、filter、reduce 等

能取代大部分 for 循环的数组方法:map、filter、reduce、find、some、every,以及哪些会修改原数组、哪些会返回新数组。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

数组自带一整套工具

JavaScript 数组带有大量内置方法。你原本要写 for 循环来做的大部分事情,比如转换值、挑出一部分、求和,都有对应的方法,一行就能搞定,可读性更好,还能和其他方法干净地组合在一起。

入门三件套是 map、filter 和 reduce。掌握它们和几个相关的方法,你那些充斥着循环的代码就能变成一眼就能看懂的样子。

每个方法都接收一个回调函数,并返回某个结果。它们都没有修改 nums,这一点值得尽早记住。

map:转换每个元素

map 接收一个函数,对每个元素调用它,并把返回值收集到一个长度相同的新数组里。当你想要“每个输入对应一个输出”时,就用它。

如果需要,回调函数还会收到索引作为第二个参数:arr.map((item, i) => ...)。不需要时忽略它就行。

一个常见错误:明明不需要返回的数组,却用了 map。如果你只是想打印每一项,或者把它们写进数据库,那应该用 forEach 或普通循环。

filter:保留符合条件的元素

filter 对每个元素调用一个谓词函数(返回 true 或 false 的函数),保留返回真值的元素。新数组的长度相同或更短。

map 和 filter 可以很自然地链式调用。把这条链从左到右当作一条流水线来读:

先 filter,再 map,这样 map 只会处理留下来的元素。

reduce:把数组归并成一个值

reduce 是三者中最通用的一个。你给它一个归并函数 (accumulator, item) => newAccumulator 和一个初始值。它遍历数组,把每一项连同目前的累加器一起传给归并函数,最后返回累加器的最终结果。

结果不一定是数字。它可以是对象、另一个数组、字符串,你构建出什么都可以:

一定要传入初始值(第二个参数)。如果不传,reduce 会把第一个元素当作初始累加器,这在空数组上会出错,而且通常也不是你想要的效果。

reduce 很强大,但逻辑一复杂就可能很难读。如果你的归并函数超过几行,普通的 for...of 循环往往更清晰。

forEach:只做副作用,不返回值

forEach 就像不返回数组的 map。它适用于你想对每一项做点什么,比如打印日志、调用 API、更新 DOM,而不需要得到一个新集合的情况。

有两点需要知道:

  • forEach 返回 undefined。你不能在它后面接着调用 .map()。
  • 你不能用 break 提前跳出 forEach。如果需要提前退出,就用 for...of 或 some/every。

如果你发现自己在写 arr.forEach(x => results.push(transform(x))),那其实应该用 map。

find 和 findIndex:只要一个

find 返回第一个符合谓词的元素,如果没有匹配的就返回 undefined。findIndex 返回它的索引(或 -1)。

find 在找到第一个匹配项时就会停止。不要用 filter(...)[0],它会扫描整个数组,只是为了把其余的结果扔掉。

some 和 every:回答是非问题

只要至少有一个元素通过测试,some 就返回 true。只有所有元素都通过时,every 才返回 true。

两者都会短路:some 遇到第一个 true 就停止,every 遇到第一个 false 就停止。它们是回答“有没有任何一个……”和“是不是全部都……”这类问题的合适工具。

slice 与 splice:复制与剪切

这两个方法名字很像,作用却完全不同。

slice(start, end) 返回数组一部分的浅拷贝。它不会修改任何东西。end 不包含在内;省略它就一直取到末尾。

splice(start, deleteCount, ...items) 会就地修改数组。它从 start 开始删除 deleteCount 个元素,可以选择插入新元素,并返回被删除的元素。

记忆方法:slice 是安全的(复制),splice 是动手术(就地修改数组)。

修改原数组与不修改原数组

这个区别很重要。不小心修改了共享数组的代码,是最难排查的 bug 之一。

修改原数组的方法(修改原数组,通常返回别的东西):

  • push、pop、shift、unshift
  • splice、sort、reverse
  • fill、copyWithin

不修改原数组的方法(返回新数组或值,原数组不变):

  • map、filter、slice、concat
  • flat、flatMap
  • find、findIndex、some、every、includes、indexOf
  • reduce、reduceRight

要特别留意的是 sort 和 reverse:它们看起来人畜无害,却会悄悄修改原数组。如果你需要一个排好序的副本,先 slice 一下:

现代 JS 还提供了不修改原数组的对应版本:toSorted、toReversed、toSpliced 和 with。它们返回新数组,原数组保持不变。所有当前的运行环境都支持它们,能用就用。

flat 和 flatMap

flat 把嵌套数组展平一层(传入深度参数可以展平更多层)。flatMap 相当于先 map 再展平一层,当每一项会映射成零个或多个输出时很有用。

flatMap 是“展开”元素(一个输入,多个输出)的简洁写法,不需要额外调用一次 flat()。

综合运用

来看一个贴近实际的小例子。给定一个订单列表,计算已完成且金额超过 $50 的订单的总收入:

三个方法,一条流水线,不需要任何循环里的记账工作。每一步都描述了它在做什么。你也可以把两次 filter 调用合并成一次,但分开写读起来也不错,调试时有时还更方便。

下一页:Map 和 Set

数组很擅长处理有序序列,但当你需要按键快速查找,或者需要一个元素不重复的集合时,它就显得笨拙了。JavaScript 有两种内置数据结构正是为此而生:Map 和 Set,这就是下一页的内容。

常见问题

JavaScript 中 map、filter 和 reduce 有什么区别?

map 转换每个元素,返回一个长度相同的新数组。filter 只保留通过测试的元素,返回一个(通常更短的)新数组。reduce 遍历数组,把它归并成一个值:求和、一个对象、另一个数组,你构建出什么就是什么。

forEach 和 map 有什么区别?

forEach 对每个元素执行一个函数,返回 undefined,它是用来产生副作用的。map 对每个元素执行一个函数,并返回由结果组成的新数组。如果你想要一个转换后的数组,就用 map。如果你只想对每个元素做点什么,并不需要结果,就用 forEach(或 for...of 循环)。

哪些数组方法会修改原数组?

会修改原数组的有 push、pop、shift、unshift、splice、sort、reverse、fill 和 copyWithin。其他方法,比如 map、filter、slice、concat、flat、flatMap、find、some、every、reduce,都不会动原数组,而是返回一个新值。

什么时候用 slice,什么时候用 splice?

slice(start, end) 返回数组某一段的浅拷贝,不会动原数组。splice(start, deleteCount, ...items) 会修改数组:它就地删除和/或插入元素,并返回被删除的元素。记忆方法:slice 是安全的复制,splice 是动手术式的修改。

Coddy 编程语言插图

和 Coddy 一起学编程

开始