一、数组扁平化的定义(速记版)
**面试速记:数组扁平化(Flatten Array)指将嵌套的多维数组转换为一维数组的操作,核心是拆解数组的嵌套层级,让所有元素都处于同一层级。**例如:
- 多维数组:
[1, [2, [3, 4], 5], 6] - 扁平化后:
[1, 2, 3, 4, 5, 6]
应用场景:
-
日常开发 :优先用
Array.prototype.flat(Infinity)(原生高效)或 Lodash(复杂场景); -
面试手写:掌握「递归法」和「reduce + 递归」,进阶可提「尾递归优化」或「Generator」;
-
临时测试:用「JSON.stringify + JSON.parse」或「正则 + split」(仅限简单数组);
-
大数据 / 惰性场景:用 Generator 生成器(按需迭代,减少内存占用)。
性能比较
递归方法:通用但可能栈溢出
flat方法:性能最佳但需现代环境
toString方法:仅限数字数组且效率较低
总结:每种方法各有优劣,选择时应考虑浏览器兼容性、数据特点和性能需求。现代项目推荐优先使用原生flat方法,旧项目可采用reduce或递归方案。
二、详解常见实现方式(原理)
方式 1:ES6 原生 flat() 方法(推荐,最简洁)
Array.prototype.flat(depth) 是 ES2019 新增的专用扁平化方法,无需手动实现核心逻辑:
- 参数
depth:可选,指定扁平化深度(默认 1);传入Infinity可处理任意深度的嵌套数组; - 返回值:新的一维数组(不修改原数组)。
javascript
运行
javascript
// 基础示例
const nestedArr = [1, [2, [3, 4], 5], 6];
// 扁平化1层(默认行为)
const flat1Layer = nestedArr.flat();
console.log(flat1Layer); // [1, 2, [3, 4], 5, 6]
// 扁平化无限层级(最常用)
const flatAll = nestedArr.flat(Infinity);
console.log(flatAll); // [1, 2, 3, 4, 5, 6]
// 自动忽略空项(额外特性)
const arrWithEmpty = [1, , [2, [3]]];
console.log(arrWithEmpty.flat(Infinity)); // [1, 2, 3]
解释:
flat()无参数时仅拆解一层嵌套;flat(Infinity)是实战中最常用的写法,适配所有嵌套深度;- 该方法会自动过滤数组中的空元素(如
[,])。
方式 2:手动递归实现(理解核心逻辑)
核心思路:遍历数组元素,若元素是数组则递归调用扁平化函数,否则直接加入结果数组(面试高频考点)。
javascript
运行
javascript
function flattenArray(arr) {
// 复制原数组,避免修改原数据
let tempArr = [...arr];
// 循环判断:数组中是否还有嵌套的数组
while (tempArr.some(item => Array.isArray(item))) {
// 扩展运算符拆解一层,重新合并为新数组
tempArr = [].concat(...tempArr);
}
return tempArr;
}
// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
解释:
- 递归终止条件:元素不是数组时,直接
push到结果; concat()用于合并递归返回的扁平化数组,避免修改原结果数组;- 相比
typeof item === 'object',Array.isArray()能准确判断数组(排除 null / 对象等干扰)。
方式 3:reduce + 递归(函数式简洁写法)
利用 reduce 的累加器特性替代手动循环,代码更简洁,符合函数式编程风格:
javascript
运行
javascript
function flattenArray(arr) {
// reduce 遍历数组,acc 是累加器(最终一维数组),item 是当前元素
return arr.reduce((acc, item) => {
// 数组元素递归扁平化,非数组元素直接返回
return acc.concat(Array.isArray(item) ? flattenArray(item) : item);
}, []); // 累加器初始值为空数组
}
// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
解释:
reduce回调的核心逻辑与递归法一致,仅用reduce替代了for...of循环;- 初始值
[]确保累加器从空数组开始,避免空指针问题。
方式 4:扩展运算符 + 循环(逐层拆解)
利用扩展运算符(...)拆解数组的第一层嵌套,循环直到无嵌套数组:
javascript
运行
javascript
function flattenArray(arr) {
// 复制原数组,避免修改原数据
let tempArr = [...arr];
// 循环判断:数组中是否还有嵌套的数组
while (tempArr.some(item => Array.isArray(item))) {
// 扩展运算符拆解一层,重新合并为新数组
tempArr = [].concat(...tempArr);
}
return tempArr;
}
// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
解释:
some()检查数组中是否存在数组类型的元素,存在则继续循环;[].concat(...tempArr)会将每一层数组 "展开"(如[1, [2,3]]→[1,2,3]);- 循环终止条件:
some()返回false(无嵌套数组)。
三、其它方法
一、JSON.stringify + JSON.parse(字符串拆解法)
核心思路
利用 JSON.stringify() 将多维数组转为无嵌套的字符串 (自动去掉数组括号),再通过正则替换掉所有括号,最后用 JSON.parse() 转回数组。
代码实现
javascript
运行
javascript
function flattenArray(arr) {
// 1. 转为JSON字符串(如[1,[2,[3]]] → "[1,[2,[3]]]")
const str = JSON.stringify(arr);
// 2. 正则替换所有[]为空,得到纯元素字符串(→ "1,2,3")
const flatStr = str.replace(/\[|\]/g, '');
// 3. 转回数组(注意:空数组需特殊处理)
return flatStr ? JSON.parse(`[${flatStr}]`) : [];
}
// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
// 空数组/空元素测试
console.log(flattenArray([])); // []
console.log(flattenArray([[], [[]]])); // []
解释与优缺点
- 核心逻辑 :JSON.stringify 会将数组的嵌套结构转为字符串形式,正则
/\[|\]/g匹配所有方括号并删除,最终还原为一维数组字符串。 - 优点:代码极简,无需递归 / 循环,适合快速实现;
- 缺点 :
- 不支持包含
undefined、function、Symbol、BigInt的数组(JSON.stringify 会忽略 / 报错这些类型); - 若元素本身包含
[/]字符(如字符串"[123]"),会被错误替换,导致结果异常; - 所有数字会被转为字符串再转回数字,少量性能损耗。
- 不支持包含
- 适用场景 :数组元素仅为数字、字符串、布尔值、null 等基础类型,且无特殊字符的简单场景。
二、forEach + 递归(遍历简化版)
核心思路
本质是「手动递归」的简化写法,用 Array.prototype.forEach 替代 for...of 循环,遍历逻辑更简洁,无需手动维护索引。
代码实现
javascript
运行
javascript
function flattenArray(arr, result = []) {
// forEach 遍历每个元素,自动跳过空项
arr.forEach(item => {
if (Array.isArray(item)) {
// 递归处理子数组,复用同一个result数组(减少内存开销)
flattenArray(item, result);
} else {
result.push(item);
}
});
return result;
}
// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
解释与优缺点
- 核心逻辑 :通过「默认参数
result = []」复用同一个结果数组,避免每次递归都创建新数组(相比 concat 更高效); - 优点:代码简洁、内存效率高,无循环索引管理;
- 缺点 :依赖递归,嵌套层级极深时(如 10000 层)会触发
Maximum call stack size exceeded栈溢出; - 适用场景:中等嵌套深度的数组,追求代码简洁性。
三、Generator 生成器(迭代器模式)
核心思路
利用 ES6 Generator 函数的「惰性迭代」特性,逐个遍历元素,遇到数组则递归 yield* 展开,最终将迭代器转为数组。
代码实现
javascript
运行
javascript
// 定义生成器函数
function* flattenGenerator(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
// yield* 递归迭代子数组的生成器
yield* flattenGenerator(item);
} else {
// 逐个产出非数组元素
yield item;
}
}
}
// 封装为扁平化函数
function flattenArray(arr) {
// 将生成器转为数组([...iterator] 或 Array.from)
return [...flattenGenerator(arr)];
}
// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
解释与优缺点
- 核心逻辑 :Generator 函数通过
yield产出单个元素,yield*可迭代另一个生成器(实现递归展开),最终通过扩展运算符消费迭代器得到一维数组; - 优点:惰性迭代(可按需获取元素,无需一次性生成完整数组)、符合迭代器设计模式;
- 缺点 :语法稍复杂,性能略低于原生
flat(); - 适用场景:需要惰性处理大数据量数组、或结合迭代器 / 异步场景的开发。
四、第三方库(Lodash):.flattenDeep/.flattenDepth
核心思路
实际开发中,若需兼容低版本环境或处理复杂边界(如空值、特殊类型),优先使用成熟的第三方库(如 Lodash),其封装了完善的扁平化方法。
代码实现
javascript
运行
javascript
// 先安装:npm i lodash
const _ = require('lodash');
const nestedArr = [1, [2, [3, 4], 5], 6];
// 扁平化所有层级(等价于 flat(Infinity))
console.log(_.flattenDeep(nestedArr)); // [1, 2, 3, 4, 5, 6]
// 指定扁平化深度(等价于 flat(2))
console.log(_.flattenDepth(nestedArr, 2)); // [1, 2, 3, 4, 5, 6]
解释与优缺点
- 核心逻辑:Lodash 内部通过优化的递归 / 循环实现,处理了空项、特殊类型(如类数组、Symbol)等边界情况;
- 优点:兼容性强、鲁棒性高、支持深度配置;
- 缺点 :需引入第三方库(增加体积,可按需引入
lodash/flattenDeep); - 适用场景:企业级项目、需兼容多环境、处理复杂数组场景。
五、正则表达式 + split(极简字符串法)
核心思路
直接将数组转为字符串,用正则去掉所有括号后按逗号分割,再转为数组(比 JSON.stringify 更直接,但类型转换需注意)。
代码实现
javascript
运行
javascript
function flattenArray(arr) {
// 转为字符串 → 去掉所有[] → 按逗号分割 → 过滤空字符串 → 转数字(按需)
return arr.toString()
.replace(/\[|\]/g, '')
.split(',')
.filter(item => item) // 过滤空项(如[,]产生的空字符串)
.map(item => Number(item)); // 若元素是数字,转回数字类型
}
// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
解释与优缺点
- 核心逻辑 :
arr.toString()会将多维数组转为1,2,3,4,5,6形式(自动去掉括号),后续通过字符串处理还原数组; - 优点:代码最短,适合快速测试;
- 缺点 :
- 所有元素会被转为字符串(需手动转回原类型);
- 若元素包含逗号(如字符串
"a,b"),会被错误分割;
- 适用场景:快速验证、元素为纯数字且无特殊字符的临时场景。
六、补充:尾递归优化(解决栈溢出)
针对递归法的「栈溢出」问题,可通过尾递归优化(ES6 严格模式支持),让递归调用在栈帧末尾执行,避免栈溢出。
代码实现
javascript
运行
javascript
// 严格模式启用尾递归优化
'use strict';
function flattenArray(arr, result = [], index = 0) {
// 遍历完成,返回结果
if (index >= arr.length) return result;
const item = arr[index];
if (Array.isArray(item)) {
// 尾递归:处理子数组,index重置为0
return flattenArray(item, result, 0);
} else {
// 非数组元素加入结果,index+1继续遍历
result.push(item);
return flattenArray(arr, result, index + 1);
}
}
// 测试(深层嵌套也不会栈溢出)
const deepArr = [1, [2, [3, [4, [5]]]]];
console.log(flattenArray(deepArr)); // [1, 2, 3, 4, 5]
核心逻辑
尾递归将「遍历索引」和「结果数组」作为参数传递,递归调用是函数最后一步操作,引擎可复用栈帧,避免栈溢出。
七、新增方法总结对比
| 实现方式 | 核心特点 | 优点 | 缺点 |
|---|---|---|---|
| JSON.stringify + JSON.parse | 字符串拆解 | 极简 | 不支持特殊类型、易被特殊字符干扰 |
| forEach + 递归 | 遍历简化、复用结果数组 | 简洁、内存高效 | 嵌套过深栈溢出 |
| Generator 生成器 | 惰性迭代、迭代器模式 | 按需取值、适配异步场景 | 语法复杂、性能略低 |
| Lodash _.flattenDeep | 成熟封装、边界处理完善 | 兼容强、鲁棒性高 | 引入第三方库 |
| 正则 + split | 字符串分割 | 代码最短 | 类型转换问题、易被逗号干扰 |
| 尾递归优化 | 解决栈溢出 | 支持深层嵌套 | 仅 ES6 严格模式生效 |
八、总结
| 实现方式 | 优点 | 适用场景 |
|---|---|---|
flat(Infinity) |
简洁、原生、高性能 | 日常开发(ES6+ 环境) |
| 手动递归 | 理解核心逻辑 | 面试手写、低版本环境兼容 |
| reduce + 递归 | 简洁、函数式风格 | 代码精简需求、函数式编程 |
| 扩展运算符 + 循环 | 直观(逐层拆解) | 理解扁平化过程的教学场景 |
日常开发优先使用 flat(Infinity);面试中需掌握递归 /reduce 实现,理解 "遍历 - 判断 - 递归 / 合并" 的核心逻辑。