先记住这个答案
定义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。
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),空间复杂度取决于最深嵌套层和临时数组个数。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。