JavaScript数组扁平化实现:从flat()到递归与栈的完整解析

作者:小括号

时间:2026-10-24

浏览:643

本文详解JavaScript数组扁平化技术,涵盖原生Array.prototype.flat()的depth参数与空位处理机制。深入剖析通过递归与栈结构手动实现flat方法的多种方案,对比for、forEach、reduce等遍历方式及数组判断技巧,提供面试常用的高阶实现代码。

文章目录

1 数组扁平化的定义

该概念的核心在于将多维数组结构转换为低维或一维结构,例如:


// 原数组是一个“三维”数组
const array = [1, 2, [3, 4, [5, 6], 7], 8, 9]
 
// 可以降成二维
newArray1 = [1, 2, 3, 4, [5, 6], 7, 8, 9]
 
// 也可以降成一维
newArray2 = [1, 2, 3, 4, 5, 6, 7, 8, 9]

这一过程在技术上也常被称为数组扁平化或数组降维。

2 JavaScript标准库中的flat()方法

JavaScript原生标准库已内置数组扁平化功能,即Array.prototype.flat()。

该方法依据指定的深度参数进行递归遍历,将当前数组及其子数组中的所有元素合并,最终返回一个全新的扁平化数组。

语法格式为:var newArray = arr.flat([depth])

参数depth为可选值,用于指定递归展开的深度层级,默认值为1。它决定了数组需要被降维的层数。

返回值是一个包含原数组及所有子数组元素的新数组。

具体示例如下:


const array = [1, 2, [3, 4, [5, 6], 7], 8, 9]
const newArray1 = array.flat() // 等价于array.flat(1);降1维
// newArray1: [1, 2, 3, 4, [ 5, 6 ], 7, 8, 9]
 
const newArray2 = array.flat(2) // 降2维
// newArray2:[1, 2, 3, 4, 5, 6, 7, 8, 9]

特殊行为说明:

当depth小于等于0时,返回的数组维数与原数组保持一致(需注意,这仅指维度结构相同,空位处理情况请参考第3点说明)。


const array = [1, 2, [3, 4, [5, 6], 7], 8, 9]
array.flat(-1)
// [1, 2, [3, 4, [5, 6], 7], 8, 9]

当depth设置为Infinity时,无论原数组多少维,最终结果都将完全展开为一维数组。


array.flat(Infinity)
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

若原数组包含空位,flat方法会将其消除,即便调用flat(0)也会消除空位,因此第1点仅强调“维数一样”。此外,flat方法仅消除已展开层级内的空位,深层未展开层级的空位将保留。


const array1 = [1, , 2, [3, ,4, [5, 6], 7], 8, 9] 
// 注意这个数组有两个空位
array.flat(0)
// [ 1, 2, [ 3,  ,4, [ 5, 6 ], 7 ], 8, 9 ]
// 第一个空位没了,第二个空位还在
 

3 手动实现flat方法

实现单层展开(降1维)的逻辑为:遍历数组,若当前元素非数组则直接保存;若为数组,则将其展开后保存。

实现多层展开(降多维)则是在单层展开的基础上,通过递归对子数组执行相同操作。

可将实现过程分解为以下三个步骤:

1、数组遍历方式的选择

2、数组类型的判断逻辑

3、递归函数的构建

组合上述三步即可得到多种flat方法的实现方案。

3.1 数组遍历方法

JavaScript提供了多种遍历方式,主要可分为三类:

1、for循环相关语法

  • 传统的for循环
  • for...of循环

for...in主要用于遍历对象属性,不建议直接用于数组遍历。


const array = [1, 2, [3, 4, [5, 6], 7], 8, 9]
// for循环
for (let i = 0; i < array.length; i++) {
    const element = array[i];
}
// for...of
for (const element of array) {
    
}

2、数组内置方法:可直接获取元素的方法

  • forEach()
  • reduce()
  • map()

// forEach()
array.forEach(element => {
    
});
// reduce()
array.reduce((pre, cur) => {
    const element = cur
}, [])
// map()
array.map(element => {
  
})

3、数组内置方法:返回遍历器(Iterator)对象的方法

  • keys()
  • values()
  • entries()

// 这三种方式仅仅是获得遍历器对象,还需搭配for...of来进行遍历
// keys()
for (let i of array.keys()) {
  const element = array[i]
}
// values()
for (let element of array.values() ) {
  
}
// entries()
for (let [i, element] of array.entries()) {
  console.log(array[i])
  console.log(element)
}

3.2 数组类型判断

假设变量为a,判断其是否为数组,有以下四种常用方法:

  • Array.isArray()是Array的静态方法,专门用于检测变量是否为数组。
  • instanceof运算符通过检测构造函数的prototype属性是否出现在实例对象的原型链上来判断。若a是数组,其原型链上必然包含Array.prototype。
  • 通过对象的constructor属性判断(注意:此方法可能失效,因为constructor属性可被手动修改)。
  • 通过Object.prototype.toString()判断,该方法返回表示对象类型的字符串。

// 方法1
Array.isArray(a)
// 方法2
a instanceof Array
// 方法3
a.constructor === Array
// 方法4
// 使用call来调用Object.prototype上的toString方法
Object.prototype.toString.call(a) === '[object Array]'
 
// 不能这么判断,因为这个toString已经覆盖了Object.prototype.toString
// 只有Object.prototype.toString能正确判断类型
a.toString()

3.3 递归逻辑

递归的核心在于对子元素执行与父元素相同的处理操作。


function flat() {
  let res = []
  遍历数组 {
    if (当前元素是数组) {
      flat(当前元素)得到一维数组
      将一维数组拼接到res中
    } else {
      res.push(当前元素)
    }
  }
  return res
}

3.4 初步实现flat方法

选择合适的遍历方式、数组判断方式并配合递归,即可初步实现flat方法,例如:


function myFlat(arr) {
  let res = [];
  for (const item of arr) {
    if (Array.isArray(item)) {
      res = res.concat(myFlat(item));
      // 注意concat方法返回一个新数组,不会改变原数组
    } else {
      res.push(item);
    }
  }
  return res;
}

上述myFlat方法可将“多维”数组完全拉平为一维,但存在两个局限:无法指定展开深度depth,且无法正确处理数组空位。

4 方法优化

4.1 支持指定展开深度

处理展开深度的逻辑较为简单,只需增加递归终止条件depth<=0,代码如下:


function myFlat(arr, depth = 1) {
  // 若depth<=0,则直接返回
  if (depth <= 0) {
      return arr
  }
  let res = [];
  for (const item of arr) {
    if (Array.isArray(item)) {
      // 每次递归调用,将depth-1
      res = res.concat(myFlat(item, depth - 1));
    } else {
      res.push(item);
    }
  }
  return res;
}

4.2 数组空位处理

在实际开发中,应尽量避免出现数组空位的情况。

前文提到的不同遍历方法对数组空位的处理机制存在差异。

forEach、reduce和map在遍历时遇到空位会直接忽略;而for...of不会忽略,会将空位视为undefined处理。

4.2.1 改进for...of遍历

因此,我们需要对基于for...of遍历的myFlat方法进行改进:


function myFlat(arr, depth = 1) {
  if (depth <= 0) {
    return arr;
  }
  let res = [];
  for (const item of arr) {
    if (Array.isArray(item)) {
      res = res.concat(myFlat(item, depth - 1));
    } else {
      // 判断数组空位
      item !== undefined && res.push(item);
    }
  }
  return res;
}

4.2.2 使用forEach或map遍历

当然,也可以改用forEach或map方法遍历数组,从而免去手动判断空位的步骤。

但需注意一个特殊情况:当depth <= 0时,需使用filter方法来消除数组空位。


// forEach
function myFlat(arr, depth = 1) {
  if (depth <= 0) {
    return arr.filter(item => item !== undefined);
  }
  let res = [];
  arr.forEach((item) => {
    if (Array.isArray(item)) {
      res = res.concat(myFlat(item, depth - 1));
    } else {
      res.push(item);
    }
  });
  return res;
}
 
// map
function myFlat(arr, depth = 1) {
  if (depth <= 0) {
    return arr.filter(item => item !== undefined);
  }
  let res = [];
  arr.map((item) => {
    if (Array.isArray(item)) {
      res = res.concat(myFlat(item, depth - 1));
    } else {
      res.push(item);
    }
  });
  return res;
}

4.2.3 使用reduce方法

其中,使用reduce方法实现的代码最为简洁,也是面试中的高频考点之一。


function myFlat(arr, depth = 1) {
  return depth > 0
    ? arr.reduce(
        (pre, cur) =>
          pre.concat(Array.isArray(cur) ? myFlat(cur, depth - 1) : cur),
        []
      )
    : arr.filter((item) => item !== undefined);
}

5 其他实现思路

5.1 栈结构实现

理论上,递归方法通常可以转换为非递归形式,即使用栈结构来实现。


function myFlat(arr) {
  let res = [];
  const stack = [].concat(arr);
  while (stack.length > 0) {
    const item = stack.pop();
    if (Array.isArray(item)) {
      // 用扩展运算符展开一层
      stack.push(...item);
    } else {
      item !== undefined && res.unshift(item);
    }
  }
  return res;
}

然而,此方法无法指定展开深度,只能将数组彻底展开为一维。

5.2 栈方法的改进

针对栈方法无法指定展开深度的缺点,可进行如下改进:


function myFlat(arr, depth = 1) {
  if (depth <= 0) {
    return arr.filter((item) => item !== undefined);
  }
  let res;
  let queue = [].concat(arr);
  while (depth > 0) {
    res = [];
    queue.forEach((item) => {
      if (Array.isArray(item)) {
        // 注意用扩展运算符将数组展开前先用filter方法去掉空位
        res.push(...item.filter((e) => e !== undefined));
      } else {
        res.push(item);
      }
    });
    depth--;
    queue = res;
  }
  return res;
}

总结

本文介绍了JavaScript数组扁平化的多种实现方式,包括原生API、递归及栈结构。更多关于JS实现数组扁平化的内容,请查阅本站相关文章或继续浏览后续内容,感谢支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章