JavaScript 数组扁平化全解析:从基础到进阶,深入理解 flat
与多种实现方式
在现代前端开发中,数组操作是日常编码中最常见的任务之一。而在处理复杂数据结构时,我们经常会遇到"嵌套数组"(即高维数组)的场景。例如,后端返回的数据结构可能是多层嵌套的,我们需要将其"拍平"为一维数组以便于渲染或进一步处理。这种将多层嵌套数组转换为单层数组的过程,就被称为 数组扁平化(Array Flattening)。
本文将带你全面了解 JavaScript 中数组扁平化的各种方法,包括原生 API 的使用、递归实现、reduce
高阶函数应用、利用 toString
和 split
的巧妙技巧,以及基于展开运算符的循环优化方案。我们将深入剖析每种方法的原理、优缺点和适用场景,帮助你构建完整的知识体系。
一、什么是数组扁平化?
数组扁平化,顾名思义,就是把一个嵌套多层的数组"压平"成一个只有一层的一维数组。例如:
js
const nestedArr = [1, [2, 3, [4, 5]], 6];
// 扁平化后应得到:
// [1, 2, 3, 4, 5, 6]
这个问题看似简单,但在实际项目中非常常见。比如你在处理树形菜单、评论回复结构、文件目录层级等数据时,都可能需要对嵌套数组进行扁平化处理。
二、使用原生 flat()
方法(推荐方式)
ES2019 引入了 Array.prototype.flat()
方法,使得数组扁平化变得极其简单和直观。
✅ 基本语法
js
arr.flat([depth])
depth
:指定要展开的层数,默认为1
。- 如果传入
Infinity
,则无论嵌套多少层,都会被完全展开。
✅ 示例代码
js
const arr = [1, [2, 3, [1]]];
console.log(arr.flat()); // [1, 2, 3, [1]] → 只展开一层
console.log(arr.flat(2)); // [1, 2, 3, 1] → 展开两层
console.log(arr.flat(Infinity)); // [1, 2, 3, 1] → 完全展开
✅ 特点总结
- 简洁高效:一行代码解决问题。
- 兼容性良好:现代浏览器基本都支持(IE 不支持)。
- 可控制深度:灵活控制展开层级。
- 推荐用于生产环境:清晰、安全、性能好。
⚠️ 注意:
flat()
不会改变原数组,而是返回一个新的扁平化数组。
三、递归实现:最经典的思路
如果你不能使用 flat()
(比如兼容老版本浏览器),或者想深入理解其内部机制,那么递归是一个经典且直观的解决方案。
✅ 基础递归版本
js
function flatten(arr) {
let res = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
res = res.concat(flatten(arr[i])); // 递归处理子数组
} else {
res.push(arr[i]); // 非数组元素直接加入结果
}
}
return res;
}
// 测试
const arr = [1, [2, 3, [1]]];
console.log(flatten(arr)); // [1, 2, 3, 1]
✅ 分析
- 使用
for
循环遍历每个元素。 - 判断是否为数组:是 → 递归调用;否 → 直接推入结果数组。
- 利用
concat
合并递归结果。
✅ 缺点
- 每次
concat
都会创建新数组,性能略低。 - 递归深度过大可能导致栈溢出(极端情况)。
四、使用 reduce
+ 递归:函数式编程风格
利用 reduce
可以写出更优雅、更具函数式风格的扁平化函数。
✅ 实现方式
js
function flatten(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);
}
✅ 解析
reduce
接收一个累加器pre
和当前元素cur
。- 如果
cur
是数组,则递归调用flatten(cur)
,否则直接使用cur
。 - 使用
concat
将结果合并到pre
中。
✅ 优点
- 代码简洁,逻辑清晰。
- 更符合函数式编程思想。
- 易于组合其他操作(如 map、filter)。
五、利用 toString()
+ split()
的"黑科技"技巧
这是一个非常巧妙但需要谨慎使用的技巧,适用于数组中只包含数字或字符串基本类型的情况。
✅ 实现原理
JavaScript 中,数组的 toString()
方法会递归地将每个元素转为字符串,并用逗号连接。
js
const arr = [1, [2, 3, [1]]];
console.log(arr.toString()); // "1,2,3,1"
我们可以利用这一点,先转成字符串,再用 split(',')
分割,最后通过 +item
转回数字。
✅ 实现代码
js
function flatten(arr) {
return arr.toString().split(',').map(item => +item);
}
// 测试
const arr = [1, [2, 3, [1]]];
console.log(flatten(arr)); // [1, 2, 3, 1]
✅ 优点
- 代码极短,实现"一行扁平化"。
- 性能较好(底层由引擎优化)。
✅ 缺点(⚠️ 重要)
- 仅适用于纯数字数组 :如果数组中有字符串
"hello"
,+"hello"
会变成NaN
。 - 无法保留原始类型:所有元素都会被转为数字。
- 丢失
null
、undefined
、对象等复杂类型信息。
❗ 所以这个方法虽然巧妙,但不适合通用场景,仅作为面试中的"奇技淫巧"了解即可。
六、使用 while
循环 + concat
+ 展开运算符(性能优化版)
这种方法避免了递归调用,采用循环逐步"拍平"数组,适合处理深层嵌套且希望避免栈溢出的场景。
✅ 实现方式
js
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
✅ 原理解析
arr.some(item => Array.isArray(item))
:检查数组中是否还存在嵌套数组。...arr
:展开数组的所有元素。[].concat(...arr)
:concat
会对展开后的数组元素自动"拍平一层"。
🔍 举个例子:
js
[].concat(...[1, [2, 3, [1]]])
// 等价于
[].concat(1, [2, 3, [1]])
// → [1, 2, 3, [1]] → 拍平了一层
然后继续循环,直到没有嵌套为止。
✅ 优点
- 非递归,避免栈溢出。
- 逻辑清晰,易于理解。
- 性能较好,尤其适合中等深度嵌套。
✅ 缺点
- 每次
concat(...arr)
都会创建新数组,内存开销较大。 - 对于极深嵌套,仍可能影响性能。
七、对比总结:各种方法的适用场景
方法 | 优点 | 缺点 | 推荐场景 |
---|---|---|---|
arr.flat(Infinity) |
简洁、标准、安全 | IE 不支持 | ✅ 生产环境首选 |
递归 + for |
逻辑清晰,易理解 | 性能一般,可能栈溢出 | 学习理解原理 |
reduce + 递归 |
函数式风格,优雅 | 同上 | 偏好函数式编程 |
toString + split |
代码短,性能好 | 类型受限,不通用 | 面试奇技淫巧 |
while + concat + ... |
非递归,避免栈溢出 | 内存占用高 | 深层嵌套处理 |
八、扩展思考:如何实现深度可控的扁平化?
有时候我们并不想完全拍平,而是只想展开指定层数。可以仿照 flat(depth)
实现一个通用函数:
js
function flattenDepth(arr, depth = 1) {
if (depth === 0) return arr.slice(); // 深度为0,直接返回副本
let result = [];
for (let item of arr) {
if (Array.isArray(item) && depth > 0) {
result.push(...flattenDepth(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}
// 测试
const arr = [1, [2, 3, [4, 5, [6]]]];
console.log(flattenDepth(arr, 1)); // [1, 2, 3, [4, 5, [6]]]
console.log(flattenDepth(arr, 2)); // [1, 2, 3, 4, 5, [6]]
console.log(flattenDepth(arr, Infinity)); // [1, 2, 3, 4, 5, 6]
九、结语
📌 小贴士 :如果你的项目需要兼容老旧浏览器,可以使用 Babel 转译 flat()
,或手动引入 polyfill:
js
// Polyfill for Array.prototype.flat
if (!Array.prototype.flat) {
Array.prototype.flat = function(depth = 1) {
return this.reduce((acc, val) =>
Array.isArray(val) && depth > 0
? acc.concat(val.flat(depth - 1))
: acc.concat(val)
, []);
};
}
这样就能在任何环境中愉快地使用 flat()
了!