JavaScript 数组扁平化全解析

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

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

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


一、什么是数组扁平化?

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

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]

✅ 优点

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

✅ 缺点(⚠️ 重要)

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

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


六、使用 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() 了!

相关推荐
前端大卫7 小时前
Vue 和 React 受控组件的区别!
前端
Hy行者勇哥7 小时前
前端代码结构详解
前端
代码AI弗森7 小时前
使用 JavaScript 构建 RAG(检索增强生成)库:原理与实现
开发语言·javascript·ecmascript
Lhy@@7 小时前
Axios 整理常用形式及涉及的参数
javascript
练习时长一年7 小时前
Spring代理的特点
java·前端·spring
水星记_8 小时前
时间轴组件开发:实现灵活的时间范围选择
前端·vue
2501_930124708 小时前
Linux之Shell编程(三)流程控制
linux·前端·chrome
潘小安8 小时前
『译』React useEffect:早知道这些调试技巧就好了
前端·react.js·面试
@大迁世界9 小时前
告别 React 中丑陋的导入路径,借助 Vite 的魔法
前端·javascript·react.js·前端框架·ecmascript
EndingCoder9 小时前
Electron Fiddle:快速实验与原型开发
前端·javascript·electron·前端框架