前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库数组扁平化 手写 flat
前端前端手写题实用函数实现

手写数组扁平化:如何用递归实现任意深度嵌套数组的拍平?

递归数组扁平化就是遍历每个元素,遇到子数组就递归调用并摊平,再把结果并入新数组。用Array.isArray判断分支,直到无嵌套为止。

前端进阶之旅 · 一题精讲更新于 2026.09.05
前端手写题#实用函数实现#测试
先看核心答案读代码示例
理解线索

递归拍平数组

  1. 数组类型判断判断每个元素是否仍为数组,是则递归
  2. 递归结果拼接把展开后的子数组逐个push进结果数组
  3. 返回新数组保持原数组不变,类似原生flat

如果数据深度过大或包含稀疏空位,基础递归并非安全选择。

核心回答

先记住这个答案

定义flatten(arr)创建空result,用for...of遍历每一项。若Array.isArray(item)为真,执行result.push(...flatten(item))把子数组递归展开后的一段段压入;否则直接push(item)。空数组自然返回空结果。整个过程不修改原输入,返回全展开的一维数组。核心是递归和类型识别。

  • 用Array.isArray递归能按层剥开嵌套数组
  • 递归返回新数组,不改变原数组
  • 深层嵌套可能爆栈,谨慎控制层级

递归调用如何逐层拆解嵌套

实现要点是遍历输入数组的每一项:遇到数组就递归调用自身,把子数组摊平后的元素顺序并入结果;遇到普通值则直接收集。因为递归每次都返回完整的一维数组,嵌套层级会逐层剥离,直到整个结构中不存在数组。这个思路对应原生flat(Infinity)的语义,不设终止深度。

为什么用result.push(...flatten(item))而不先concat再赋值?展开操作会把递归产生的数组元素按原序逐项加入result,不产生多余数组引用;concat同样可行但会额外创建一个新数组再拷贝。真实差异在深层嵌套时更明显,不过两者都不会修改原输入。若担心展开操作大量参数可能导致调用栈溢出,可以改成for循环遍历递归结果再逐个push。

递归扁平化基础实现JavaScript
function flatten(arr) {
  const result = [];
  for (const item of arr) {
    if (Array.isArray(item)) {
      result.push(...flatten(item));
    } else {
      result.push(item);
    }
  }
  return result;
}

console.log(JSON.stringify(flatten([1, [2, [3, [4]]]])));
console.log(JSON.stringify(flatten([1, [], [2, [3]], 4])));
console.log(JSON.stringify(flatten([])));
console.log(JSON.stringify(flatten([['a'], {b: 1}, [['c']]])));
查看输出与解释
[1,2,3,4]
[1,2,3,4]
[]
["a",{"b":1},"c"]

代码用for...of遍历,稀疏空位会被当作undefined保留(与原生flat的移除行为不同)。每次递归返回值经展开运算符并入结果,保持顺序。测试包含嵌套、空数组、空输入和非数组对象。

拍平权限配置中的多级标签组

假设后台权限模块把一组地区标签存成三层嵌套:['全部', ['华东', ['杭州', '宁波']], ['华南', ['深圳']]]。导出报表前需要把标签合并成一维数组传给筛选接口。使用递归flatten(allTags)调用返回全新的一维数组,不管配置维护后新增到几层,都无需改动外层业务代码。

选择手写递归而不是原生flat,是为了在部分旧浏览器或闭卷环境中给出可直接运行实现。此时要特别注意输出结果:如果配置里混有数字型的权限id,递归会原样保留数字,而如果改用join或toString来拍平就会被强制转成字符串,导致后续类型校验出错。所以这一步的价值是保留元素类型。

递归深度与空位语义的边界

递归最大的代价是调用栈。嵌套每一层会压入一个执行帧,若数据由程序循环动态生成并超过约一万层(如[1, [2, [3, ...]]]),执行会抛出RangeError: Maximum call stack size exceeded。因此实现前要先评估数据深度,若不安全应采用显式栈的迭代实现,这样每层只占用堆内存而不消耗调用栈。

另一个容易忽视的边界是稀疏数组。原生flat会清空跳过的空位,而用for...of遍历的普通递归会把空位读到undefined并保留。要完全模拟原生语义,应改用索引for循环并检查index in array,但这样做会使基础版代码变复杂。通常服务端JSON不含空位,本地测试如果构造[1, , 2]需先想明白结果是否符合预期。

回答前,多想一步

容易答错的地方

用 typeof item === 'array' 判断数组
typeof对数组总是返回'object',根本不会返回'array'。应该用Array.isArray或Object.prototype.toString.call(item) === '[object Array]'。后者跨iframe也可靠,但更繁琐;instanceof Array在多窗口环境有边界问题。
认为结果数组通过引用修改了原数组
递归版通常创建新数组返回,不会改传入的数组。若误以为原地拍平,直接把flatten(arr)当语句调用却没有用返回值,就白白丢失结果。正确做法是把返回值赋给变量再使用。
试着用自己的话回答

面试官还会怎么问?

怎么扩展为支持指定深度的递归?

给flatten增加第二参数depth,默认1。遍历时仅当元素为数组且depth大于0时,才递归调用flatten(item, depth - 1)并展开结果,否则直接放入结果。depth为0时不展开任何子数组,相当于仅返回原数组元素的浅拷贝。

如果传入的不是数组而是类数组对象会怎样?

当前实现期望传入真数组,通过for...of迭代。若元素是类数组对象,因Array.isArray返回false,会被原样保留,不展开其内部结构。原生flat可直接作用于类数组(通过Array.prototype.flat.call),它会读取索引并展开其中的真数组元素,但非真数组元素同样保留。两者对非真数组元素都不做展开,但原生flat还允许将类数组本身作为this,而手写函数无法直接处理类数组。

这个递归实现的性能瓶颈在哪?

每个元素都被访问一次,但每次递归调用都生成一个临时结果数组,展开运算符会让函数参数进入栈,元素多时可能触发参数上限。总体时间复杂度O(n),空间复杂度取决于最深嵌套层和临时数组个数。

从一道题,走向一组知识

把知识连起来

实用函数实现

手写 JavaScript 深拷贝:如何处理普通对象与数组的递归克隆?

同属「实用函数实现」专题,接着看 JavaScript 深拷贝 手写 在具体场景中的处理方式。

实用函数实现

手写深拷贝时如何用 WeakMap 解决循环引用问题?

同属「实用函数实现」专题,接着看 深拷贝 循环引用 WeakMap 在具体场景中的处理方式。

放回知识体系中复习JavaScript 进阶知识

参考资料

  • Array.prototype.flat()

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 递归调用如何逐层拆解嵌套
  3. 拍平权限配置中的多级标签组
  4. 递归深度与空位语义的边界
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑