数组自带一整套工具
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、unshiftsplice、sort、reversefill、copyWithin
不修改原数组的方法(返回新数组或值,原数组不变):
map、filter、slice、concatflat、flatMapfind、findIndex、some、every、includes、indexOfreduce、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 是动手术式的修改。