注册
web

JavaScript 数组扁平化全解析

JavaScript 数组扁平化全解析:从基础到进阶,深入理解 flat 与多种实现方式


在现代前端开发中,数组操作是日常编码中最常见的任务之一。而在处理复杂数据结构时,我们经常会遇到“嵌套数组”(即高维数组)的场景。例如,后端返回的数据结构可能是多层嵌套的,我们需要将其“拍平”为一维数组以便于渲染或进一步处理。这种将多层嵌套数组转换为单层数组的过程,就被称为 数组扁平化(Array Flattening)


本文将带你全面了解 JavaScript 中数组扁平化的各种方法,包括原生 API 的使用、递归实现、reduce 高阶函数应用、利用 toStringsplit 的巧妙技巧,以及基于展开运算符的循环优化方案。我们将深入剖析每种方法的原理、优缺点和适用场景,帮助你构建完整的知识体系。




一、什么是数组扁平化?


数组扁平化,顾名思义,就是把一个嵌套多层的数组“压平”成一个只有一层的一维数组。例如:


const nestedArr = [1, [2, 3, [4, 5]], 6];
// 扁平化后应得到:
// [1, 2, 3, 4, 5, 6]

这个问题看似简单,但在实际项目中非常常见。比如你在处理树形菜单、评论回复结构、文件目录层级等数据时,都可能需要对嵌套数组进行扁平化处理。




二、使用原生 flat() 方法(推荐方式)


ES2019 引入了 Array.prototype.flat() 方法,使得数组扁平化变得极其简单和直观。


✅ 基本语法


arr.flat([depth])


  • depth:指定要展开的层数,默认为 1
  • 如果传入 Infinity,则无论嵌套多少层,都会被完全展开。

✅ 示例代码


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()(比如兼容老版本浏览器),或者想深入理解其内部机制,那么递归是一个经典且直观的解决方案。


✅ 基础递归版本


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 可以写出更优雅、更具函数式风格的扁平化函数。


✅ 实现方式


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() 方法会递归地将每个元素转为字符串,并用逗号连接


const arr = [1, [2, 3, [1]]];
console.log(arr.toString()); // "1,2,3,1"

我们可以利用这一点,先转成字符串,再用 split(',') 分割,最后通过 +item 转回数字。


✅ 实现代码


function flatten(arr) {
return arr.toString().split(',').map(item => +item);
}

// 测试
const arr = [1, [2, 3, [1]]];
console.log(flatten(arr)); // [1, 2, 3, 1]

✅ 优点



  • 代码极短,实现“一行扁平化”。
  • 性能较好(底层由引擎优化)。

✅ 缺点(⚠️ 重要)



  1. 仅适用于纯数字数组:如果数组中有字符串 "hello"+"hello" 会变成 NaN
  2. 无法保留原始类型:所有元素都会被转为数字。
  3. 丢失 nullundefined、对象等复杂类型信息


❗ 所以这个方法虽然巧妙,但不适合通用场景,仅作为面试中的“奇技淫巧”了解即可。





六、使用 while 循环 + concat + 展开运算符(性能优化版)


这种方法避免了递归调用,采用循环逐步“拍平”数组,适合处理深层嵌套且希望避免栈溢出的场景。


✅ 实现方式


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 会对展开后的数组元素自动“拍平一层”。

🔍 举个例子:

[].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) 实现一个通用函数:


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:


// 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() 了!


作者:yyt_
来源:juejin.cn/post/7543941409930625087

0 个评论

要回复文章请先登录注册