先记住这个答案
Array.prototype.flat 的默认深度是 1,只把第一层子数组的元素拼进新数组,更深的嵌套保持原样。要展开更多层可以传 2、3,或者传 Infinity 一直拍平到底。flatMap(fn) 的结果等价于 arr.map(fn).flat(1):先对每个元素调用回调,再把返回的数组展开一层。区别在于 flatMap 只遍历一次、不会生成 map 那一步的中间数组,而且深度固定是 1,不能调整。因为回调可以返回空数组或多元素数组,flatMap 能同时完成过滤和一对多映射,结果长度可以和原数组不同。
flat()默认深度 1,flat(Infinity)拍到底flatMap(fn)结果等于map(fn).flat(1)flatMap深度固定为 1,第二个参数是thisArg- 回调返回
[]就是删除,返回多项就是展开
flat 的深度参数
用一个四层嵌套的数组看不同深度的效果。
const nested = [1, [2, [3, [4]]]]
console.log(JSON.stringify(nested.flat())) // 默认深度 1
console.log(JSON.stringify(nested.flat(2)))
console.log(JSON.stringify(nested.flat(Infinity))) // 全部拍平
console.log(JSON.stringify([1, , 3].flat())) // 空位被去掉查看输出与解释
[1,2,[3,[4]]]
[1,2,3,[4]]
[1,2,3,4]
[1,3]深度可以理解成「拆几层包装」。flat() 拆一层,[2, [3, [4]]] 被拆开,里面的 [3, [4]] 原样保留。拍到底用 Infinity,但数据层级来自用户输入或者接口时要当心,特别深的嵌套会让递归开销变大。
最后一行是个容易忽略的副作用:flat 会去掉空位,结果里只剩实际存在的元素。所以 arr.flat(0) 有时被用来「压缩」稀疏数组,不过写 arr.filter(() => true) 意图更清楚。
flatMap 和 map 加 flat
flatMap 最常见的用途是「一项变多项」和「映射时顺便过滤」。
const words = ['hello world', 'hi']
console.log(JSON.stringify(words.map((s) => s.split(' ')))) // 嵌套了
console.log(JSON.stringify(words.flatMap((s) => s.split(' ')))) // 拆开一层
// 返回 [] 相当于删除,返回两项相当于展开
console.log(JSON.stringify([1, 2, 3].flatMap((x) => (x === 2 ? [] : [x, x * 10]))))
console.log(JSON.stringify([[1], [2]].flatMap((x) => [x]))) // 只拆一层查看输出与解释
[["hello","world"],["hi"]]
["hello","world","hi"]
[1,10,3,30]
[[1],[2]]把一句话拆成单词、把订单拆成商品行这类场景,map 会得到二维数组,flatMap 直接得到平铺的结果。第三行里,回调对 2 返回空数组,展开后什么都没有,2 就被删掉了;对其它值返回两项,结果就多出一项。
最后一行说明深度固定为 1。回调返回 [[1]],只拆掉最外层,剩下 [1]。需要更深的展开,只能写回 map(fn).flat(depth)。
- 为什么不直接用
map加flat - 结果一样,但
map会先生成一个完整的中间数组,flat再遍历它生成最终数组。flatMap在一次遍历里直接写出最终结果,少一个中间数组。数据量不大时差别感知不到,主要好处还是读起来更直接。
容易答错的地方
- 以为
flat()默认会全部拍平 - 默认只拆一层。
[1, [2, [3]]].flat()得到[1, 2, [3]],不是[1, 2, 3]。从 lodash 的flattenDeep迁移过来时最容易写错。 - 给
flatMap传深度 flatMap的第二个参数是thisArg,用来指定回调里的this。所以arr.flatMap(fn, 2)不会展开两层,那个 2 只是被当成了this,深度仍然是 1。- 以为回调返回的任何可迭代对象都会被展开
- 只有数组会被展开。回调返回
new Set([1, 2])或者字符串'ab',都会原样作为一项放进结果。要展开Set,在回调里先写[...set]。
面试官还会怎么问?
不用 flat,手写一个指定深度的拍平?
用 reduce 递归:遍历每一项,如果是数组并且 depth > 0,就对它递归调用并传 depth - 1,把结果拼进累加器;否则直接 push。深度很大时可以改成用栈迭代,避免递归层数太深。
flat 会改变原数组吗?
不会。flat 和 flatMap 都返回新数组,原数组不变。但新数组是浅拷贝,里面没被拆开的子数组和对象仍然是原来的引用。
flatMap 和 filter 加 map 该怎么选?
先过滤后映射,而且每项只对应零或一个结果时,filter(...).map(...) 的意图最清楚。需要一项对应多项,或者过滤条件依赖映射时才算出来的值,flatMap 更合适,因为它能在一个回调里同时决定「要不要」和「变成什么」。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。