前端必会!数组扁平化的 N 种玩法
在前端开发中,我们经常会遇到多层嵌套的数组,比如后台返回的 [1, [2, [3, 4], 5]],直接处理这种 “套娃” 数据简直让人头大。这时候就需要数组扁平化来救场 —— 简单说,就是把多维数组 “拍平” 成一维数组,比如把刚才的例子变成 [1, 2, 3, 4, 5]。不管是处理表格数据、筛选列表,还是格式化接口返回值,数组扁平化都是必备技能。今天就带大家解锁实现数组扁平化的 5 种常用方法,从基础到进阶,手把手教你搞定 “套娃” 数组~
一、数组扁平化是什么?
先给新手朋友补个基础:数组扁平化就是将多层嵌套的数组转换为一维数组的过程。
举个直观的例子:
-
原始多维数组:
[1, [2, [3, [4]]], 5] -
扁平化后数组:
[1, 2, 3, 4, 5]
它的核心作用是简化数据结构 —— 比如后台返回的树形结构数据,我们需要提取所有叶子节点时,扁平化就能帮我们快速把嵌套数据 “捋直”,避免写多层循环嵌套啦~
二、方法一:递归大法(最易理解版)
递归应该是大家接触最早的 “降维” 思路了,核心逻辑很简单:遇到数组就 “钻进去”,遇到普通元素就 “捞出来”,像剥洋葱一样一层一层处理。
(一)实现步骤
-
初始化一个空数组
res,用来存放最终的一维数组; -
遍历需要扁平化的原数组
arr; -
对每个元素做判断:
- 如果不是数组(比如数字、字符串),直接用
push加入res; - 如果是数组,就递归调用自己(把当前子数组传进去),再用
concat把返回的结果拼接到res里;
- 如果不是数组(比如数字、字符串),直接用
-
遍历结束后,返回
res。
(二)代码实现
// 定义扁平化函数,接收需要处理的数组arr
const flatten = (arr) => {
// 初始化空数组,用于存储最终的一维结果
let res = [];
// 遍历原数组的每一个元素
for (let item of arr) {
// 判断当前元素是否为数组(Array.isArray是ES5新增的数组判断方法,比typeof更准确)
if (Array.isArray(item)) {
// 若是数组,递归调用flatten处理,再把结果拼接到res中
res = res.concat(flatten(item));
} else {
// 若不是数组,直接把元素加入res
res.push(item);
}
}
// 返回最终的一维数组
return res;
};
// 测试一下:处理3层嵌套数组
console.log(flatten([1, [2, [3, 4], 5]])); // 输出 [1,2,3,4,5],完美~
(三)优缺点分析
- 优点:逻辑直白,几乎不用想就能看懂,新手友好,处理任意嵌套深度都没问题;
- 缺点:如果数组嵌套特别深(比如嵌套 1000 层),会触发浏览器的 “栈溢出” 错误(递归依赖调用栈,栈的容量有限),而且性能比非递归方法略差。
三、方法二:reduce 魔法(代码精简版)
如果你觉得递归的 for 循环不够 “优雅”,那 reduce 绝对是你的菜!reduce 本身就是用来 “合并” 数组的方法,刚好能用来做扁平化,一行代码就能搞定~
(一)reduce 方法简介
先快速回顾 reduce 的基础:它接收一个回调函数和初始值,遍历数组时会把 “上一次的结果” 和 “当前元素” 传给回调,最终返回一个合并后的值。比如用 reduce 求和:
[1,2,3].reduce((acc, cur) => acc + cur, 0); // 输出 6
这里的 acc 是 “累加器”(上一次的结果),cur 是 “当前元素”,0 是初始值。
(二)实现思路
把扁平化的逻辑融入 reduce 的回调:
-
初始值设为
[](空数组,也就是最终要返回的一维数组); -
对每个元素
cur判断:- 若是数组,就递归调用
flatten处理,再用concat拼到acc里; - 若不是数组,直接把
cur拼到acc里;
- 若是数组,就递归调用
-
每次回调返回更新后的
acc,最终就能得到一维数组。
(三)代码展示
// 用reduce实现,一行代码搞定(也可以拆成多行方便阅读)
const flatten = (arr) =>
arr.reduce((acc, cur) => {
// 判断当前元素是否为数组:是则递归处理,否则直接拼接到acc
return acc.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []); // 初始值设为空数组,作为累加器的起点
// 测试:处理混合嵌套的数组
console.log(flatten([1, [2, 'a', [true, [null]]], 3]));
// 输出 [1,2,"a",true,null,3],连非数字元素都能搞定~
和递归方法比,reduce 版去掉了显式的 for 循环和 res 变量,代码更精简,但核心逻辑其实和递归一致,所以优缺点也基本相同(同样可能栈溢出)。
四、方法三:栈模拟递归(性能优化版)
刚才的两种方法都依赖递归,遇到深层嵌套会有栈溢出风险。那有没有非递归的方法呢?当然有!用 “栈” 这种数据结构就能模拟递归的过程,还能避免栈溢出~
(一)栈的概念引入
栈是一种 “后进先出”(LIFO)的数据结构,就像叠盘子:最后放上去的盘子,要先拿下来。我们可以用栈来存储 “待处理的数组元素”,每次从栈顶取元素处理,直到栈空为止。
(二)实现过程
-
把原数组复制一份存入栈(用扩展运算符
[...arr]避免修改原数组); -
初始化空数组
res存放结果; -
循环处理栈:
- 从栈顶弹出一个元素(用
pop(),因为pop是 O (1) 操作,比shift高效); - 如果弹出的元素是数组,就把它的所有元素 “压回” 栈(用
push(...item),展开数组后压栈); - 如果不是数组,就把它加入
res;
- 从栈顶弹出一个元素(用
-
循环结束后,
res里的元素是 “倒序” 的(因为栈是后进先出),所以最后要reverse()反转一下; -
返回反转后的
res。
(三)代码实例
const flatten = (arr) => {
// 1. 初始化栈:复制原数组,避免修改原数组
const stack = [...arr];
// 2. 初始化结果数组
const res = [];
// 3. 循环处理栈,直到栈为空
while (stack.length > 0) {
// 从栈顶弹出一个元素(pop()效率高,直接操作数组末尾)
const item = stack.pop();
if (Array.isArray(item)) {
// 若是数组,把它的元素展开后压回栈(相当于“拆一层”)
stack.push(...item);
} else {
// 若不是数组,加入结果数组
res.push(item);
}
}
// 4. 反转结果数组(因为栈是后进先出,结果是倒序的)
return res.reverse();
};
// 测试:处理10层嵌套的数组(递归可能栈溢出,栈方法没问题)
const deepArr = [1, [2, [3, [4, [5, [6, [7, [8, [9, [10]]]]]]]]];
console.log(flatten(deepArr)); // 输出 [1,2,3,4,5,6,7,8,9,10],稳得一批~
(四)优缺点分析
- 优点:非递归,不会出现栈溢出问题,处理深层嵌套数组更安全;
pop和push都是高效操作,性能比递归好; - 缺点:需要手动处理栈和反转结果,逻辑比递归稍复杂一点,新手可能要多琢磨两下。
五、方法四:ES6 flat 闪亮登场(官方偷懒版)
如果你用的是现代浏览器或打包工具(比如 Webpack),那根本不用自己写 ——ES6 已经内置了 flat() 方法,专门用来做数组扁平化,简直是 “偷懒神器”!
(一)flat 方法介绍
flat(depth) 接收一个可选参数 depth,表示 “要展开的层数”:
- 默认值是
1,只展开一层数组(比如[1, [2, [3]]].flat()会变成[1, 2, [3]]); - 传
Infinity(无限大)可以展开任意深度的数组; - 传负数(比如
-1)不会展开任何层,返回原数组。
(二)使用示例
// 1. 默认展开1层
const arr1 = [1, [2, [3, 4]]];
console.log(arr1.flat()); // 输出 [1, 2, [3, 4]]
// 2. 展开2层
console.log(arr1.flat(2)); // 输出 [1, 2, 3, 4]
// 3. 展开任意深度(推荐用Infinity,不用算嵌套层数)
const deepArr = [1, [2, [3, [4, [5]]]]];
console.log(deepArr.flat(Infinity)); // 输出 [1, 2, 3, 4, 5]
// 4. 处理空元素(小知识点:flat()会自动过滤数组中的空元素!)
const arrWithEmpty = [1, [], [2, [3], null]];
console.log(arrWithEmpty.flat(Infinity)); // 输出 [1, 2, 3, null](空数组[]被过滤了)
(三)兼容性说明
flat() 是 ES2019(ES10)的特性,所以:
- 现代浏览器(Chrome 69+、Firefox 62+、Safari 12+)都支持;
- IE 浏览器完全不支持(毕竟 IE 都被放弃了);
- 如果需要兼容旧浏览器,要么用前面的递归 / 栈方法,要么给数组原型加个
flat垫片(polyfill)。
六、方法五:some + 扩展运算符(循环拆解版)
还有一种 “暴力拆解” 的思路:用 some() 判断数组里是否还有嵌套数组,如果有,就用扩展运算符 ... 拆一层,循环直到没有嵌套为止。
(一)实现原理
some(Array.isArray)可以快速判断数组中是否存在 “子数组”(只要有一个元素是数组,就返回true);- 扩展运算符
...可以把数组 “展开一层”(比如[1, [2, 3]]用...展开后是1, [2, 3]); - 用
while循环:只要数组里还有子数组,就用concat(...arr)拆一层,直到some()返回false(没有子数组了)。
(二)代码实现
const flatten = (arr) => {
// 循环:只要数组中还有子数组,就继续拆解
while (arr.some(item => Array.isArray(item))) {
// 用concat + 扩展运算符拆一层:[1, [2, [3]]] → [1, 2, [3]]
arr = [].concat(...arr);
}
return arr;
};
// 测试:处理混合嵌套
console.log(flatten([1, [2, 'a', [true]]])); // 输出 [1, 2, "a", true]
(三)优缺点分析
- 优点:代码简洁,不用递归,理解起来也不难;
- 缺点:每次循环只拆一层,嵌套深的话循环次数多,性能不如栈方法;而且会修改原数组(如果不想修改,可以先复制一份,比如
let temp = [...arr];再循环处理temp)。
七、方法大比拼(该选哪种?)
讲了 5 种方法,实际开发中该怎么选?这里整理了一张对比表,帮你快速决策:
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 递归 | 逻辑简单,易理解 | 深层嵌套会栈溢出,性能一般 | 嵌套浅、数据量小的场景,新手练手 |
| reduce + 递归 | 代码精简,易理解 | 同递归,可能栈溢出 | 同上,追求代码优雅时 |
| 栈模拟 | 无栈溢出,性能好 | 逻辑稍复杂 | 嵌套深、数据量大的场景,追求性能 |
| ES6 flat() | 一行代码,自动过滤空元素 | 兼容性差(不支持 IE) | 现代浏览器 / 打包项目,快速开发 |
| some + 扩展运算符 | 代码简洁,非递归 | 性能一般,可能修改原数组 | 嵌套不深,追求代码简洁时 |
八、总结
数组扁平化虽然是个小知识点,但背后藏着不同的编程思路:递归的 “分而治之”、reduce 的 “累加合并”、栈的 “迭代处理”、flat 的 “官方 API”。理解这些方法的原理,不仅能帮你在开发中灵活选型,还能在面试中轻松应对 “手写数组扁平化” 的问题~