实现数组 flatten 方法
基础知识
什么是数组扁平化? 数组扁平化(Flatten)是指将一个包含嵌套数组的数组,转换为一个不包含嵌套数组的一维新数组。
// 原始数组 const nestedArray = [1, [2, 3], [4, [5, 6]]]; // 扁平化后的数组 const flattenedArray = [1, 2, 3, 4, 5, 6];
扁平化的深度 (Depth) 我们可以选择将数组扁平化到指定的深度。
- 完全扁平化: 将所有层级的嵌套数组都展开,如上例所示。
- 扁平化指定深度: 只展开指定层数的嵌套。
const nestedArray = [1, [2, 3], [4, [5, 6]]]; // 只扁平化一层 const oneLevelFlat = [1, 2, 3, 4, [5, 6]];
ES2019 内置方法:
Array.prototype.flat()
现代 JavaScript 已经内置了flat()
方法,可以直接使用。它非常方便,也是我们自己实现功能时的对标标准。arr.flat()
: 默认扁平化一层。arr.flat(depth)
: 扁平化指定的depth
深度。arr.flat(Infinity)
: 完全扁平化所有层级。
核心思路
手动实现数组扁平化,主要有以下几种核心思路:
递归 (Recursion): 这是最直观、最容易理解的思路。
- 遍历数组的每一个元素。
- 判断当前元素是否还是一个数组。
- 如果是数组,就对这个子数组递归调用扁平化函数,并将返回的结果合并到最终结果中。
- 如果不是数组,就直接将其推入最终结果数组中。
- 这是实现完全扁平化的经典方法。
迭代与栈 (Iteration with a Stack): 这种方式可以避免递归过深导致的“栈溢出”问题。
- 创建一个栈(可以用数组模拟),并将原始数组作为初始元素压入。
- 当栈不为空时,循环执行以下操作:
- 从栈顶弹出一个元素。
- 判断该元素是否为数组。
- 如果是数组,则将其所有元素逆序推入栈中(逆序是为了保证最终结果的顺序正确)。
- 如果不是数组,则将其收集到结果数组中。
- 由于结果是反向收集的,最后需要将结果数组反转一次。
利用
reduce
: 这是一种更具函数式编程风格的实现方式。- 使用
Array.prototype.reduce
方法遍历数组。 reduce
的累加器是一个新数组(我们的最终结果)。- 在每次迭代中,检查当前元素是否为数组。
- 如果是数组,就递归地扁平化它,并将其与累加器数组合并(
concat
)。 - 如果不是数组,就直接将其与累加器数组合并。
- 使用
代码实现
下面我们提供几种不同思路的实现。
版本一:递归实现 (最经典)
/**
* 递归实现数组完全扁平化
* @param {Array} arr - 需要扁平化的数组
* @returns {Array} - 返回一个新的扁平化后的数组
*/
function flattenByRecursion(arr) {
let result = [];
arr.forEach((item) => {
// 1. 判断元素是否为数组
if (Array.isArray(item)) {
// 2. 如果是数组,则递归调用,并合并结果
result = result.concat(flattenByRecursion(item));
} else {
// 3. 如果不是数组,直接推入结果数组
result.push(item);
}
});
return result;
}
版本二:使用 reduce
实现
/**
* 使用 reduce 实现数组完全扁平化
* @param {Array} arr - 需要扁平化的数组
* @returns {Array} - 返回一个新的扁平化后的数组
*/
function flattenByReduce(arr) {
return arr.reduce((accumulator, current) => {
return accumulator.concat(
Array.isArray(current) ? flattenByReduce(current) : current
);
}, []); // 初始值是一个空数组
}
版本三:迭代与栈实现 (非递归)
/**
* 使用迭代和栈实现数组完全扁平化,避免栈溢出
* @param {Array} arr - 需要扁平化的数组
* @returns {Array} - 返回一个新的扁平化后的数组
*/
function flattenByStack(arr) {
const stack = [...arr]; // 将原始数组元素复制到栈中
const result = [];
while (stack.length > 0) {
// 从栈顶取出一个元素
const item = stack.pop();
if (Array.isArray(item)) {
// 如果是数组,将其元素逆序推入栈中
// 使用 spread operator (...) 可以方便地将元素展开
stack.push(...item);
} else {
// 如果不是数组,则收集到结果中
result.push(item);
}
}
// 因为是从栈顶弹出,所以结果是反的,需要反转一次
return result.reverse();
}
代码解析
递归版解析
- 数组判断:
if (Array.isArray(item))
使用Array.isArray()
是判断值是否为数组的最可靠方法,避免了instanceof
在跨窗口/框架环境中的问题。 - 递归核心逻辑:
result.concat(flattenByRecursion(item))
当遇到子数组时,不是直接添加到结果,而是递归调用自身将子数组拍平,再通过concat
合并到主结果数组,实现多层嵌套数组的扁平化。 - 递归出口:
result.push(item)
当元素不是数组时,递归结束,直接将元素收集到结果数组,作为递归终止条件。
reduce
版解析
- reduce 特性利用:
arr.reduce((accumulator, current) => { ... }, [])
使用 reduce 方法从左到右遍历数组,累加器accumulator
初始化为空数组,用于收集扁平化结果。 - 累加器与当前值:
accumulator
:最终返回的扁平化数组,每次迭代更新为新的合并数组。current
:当前处理的数组元素,可能是基本类型或子数组。
- 三元运算符处理:
- 若
current
是数组,递归调用flattenByReduce(current)
拍平子数组,再与accumulator
合并。 - 若
current
不是数组,直接与accumulator
合并,利用concat
自动将非数组元素转为单元素数组合并。
- 若
迭代与栈版解析
- 栈初始化:
const stack = [...arr]
将原始数组元素"摊平"一层放入栈中,作为处理起点,模拟递归调用栈。 - 迭代处理循环:
while (stack.length > 0)
只要栈中有待处理元素,就从栈顶取出元素处理,避免递归可能导致的栈溢出(如处理深度嵌套数组)。 - 栈操作逻辑:
stack.pop()
:从栈顶取出元素处理,由于栈的后进先出特性,处理顺序与原始数组顺序相反。stack.push(...item)
:若取出的是数组,用展开语法...item
将其元素逐个入栈,确保子数组元素被正确分解处理。
- 结果顺序调整:
result.reverse()
由于从栈顶(数组末尾)开始处理,收集的结果顺序与原始数组相反,需调用reverse()
恢复正确顺序。