在 JavaScript 开发中,处理嵌套数组是常见需求,数组扁平化就是将多层嵌套的数组转换为一维数组的过程。ES2019 引入的Array.prototype.flat()方法为这一需求提供了原生解决方案,但理解其原理并掌握自定义实现方式,能帮助开发者更灵活地应对复杂场景。本文将从基础用法出发,逐步深入到多种自定义扁平化函数的实现思路,最终实现对数组扁平化的全面掌控。
官方文档对flat的解释\]([Array.prototype.flat() - JavaScript \| MDN](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FJavaScript%2FReference%2FGlobal_Objects%2FArray%2Fflat "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat"))
一、初步认识 flat() 方法: 基础用法与局限性
flat() 方法是 JavaScript 数组原型上的原生方法,其核心作用是 "拉平" 嵌套数组,返回一个新的一维或指定深度的数组,不会改变原数组。
1.1 基本的语法
flat() 方法的语法格式如下
js
const newArray = arr.flat([depth]) ;
-
depth(可选): 默认为1的深度,填需要的深度将数组扁平化该深度,如果是 Infinite 则为扁平化为一维数组,小于等于(<=0) 则不扁平化
-
返回值 :一个扁平化后的数组,注意不改变原数组
1.2 一些开发场景
- 小坤和小鸭下蛋,需要知道一共下了多少蛋(1层)
js
const kun = ["egg","egg","egg"]
const Yaya = ["egg"]
const all = [kun,Yaya]
console.log(all.flat(1).length)// 4枚蛋
- 大鱼吃小鱼,鲲一共吃了多少条小鲤鱼 (Infinite 层)
js
const middleFish = ["fish","fish","fish"]
const bigFish = [middleFish,middleFish,middleFish]
const kun = [bigFish,bigFish,bigFish]
const all = [kun]
console.log(all.flat(Infinite).length)// 27条🐟
- 处理原数组中的空元素
js
const arr4 = [1, 2, , [3, 4, , 5]];
const flatArr4 = arr4.flat();
console.log(flatArr4); // [1, 2, 3, 4, 5](空元素已被移除)
1.3 原生 flat () 的局限性
- 浏览器兼容问题:作为 ES2019的新特性,部分的旧版本浏览器和IE浏览器不支持该方法。
- 只能处理数组嵌套,无法对扁平化过程中的元素进行自定义的处理(比如筛选一些元素)。
- 不能根据元素类型动态的调整扁平化逻辑。(比如无法处理对象)
正是因为这些局限性,促使作者写这一篇文章进行学习。
二、实现一份扁平化函数(仅数组)
1.1 实现数组的扁平化
自定义数组扁平化的核心思路是遍历数组元素,判断元素是否为数组,若为数组则继续深入遍历,直至所有元素均为非数组类型。根据遍历方式的不同,可分为递归实现、迭代实现、利用数组方法实现等多种方案,每种方案各有优劣,适用于不同场景。
方案一: 利用递归实现
递归实现的算法思想类似深度优先遍历(DFS)
想体验深度遍历可以看看这道二叉树的简单题 \]([144. 二叉树的前序遍历 - 力扣(LeetCode)](https://link.juejin.cn?target=https%3A%2F%2Fleetcode.cn%2Fproblems%2Fbinary-tree-preorder-traversal%2F "https://leetcode.cn/problems/binary-tree-preorder-traversal/"))
js
Array.prototype.myflat = function (depth = 1) {
//默认只扁平化一层
if (depth <= 0) {
return this.slice();
}
//depth 小于等于零时 直接返回浅拷贝
let res = []; //结果数组
for (let i = 0; i < this.length; i++) {
if (Array.isArray(this[i])) {
res.push(...this[i].myflat(depth - 1))//递归实现深层遍历
} else if (this[i] !== undefined && this[i] !== null){
//因为 flat() 函数有去空的功能所以加上这个判断
res.push(this[i])// 如果不是数组 则添加到结果数组 (只考虑数组)
}
}
return res
};
const arr = [1, [2, [3, [4]], 5], 6];
console.log(arr.myflat(Infinite)); // [1, 2, 3, 4, 5, 6](无限深度)
console.log(arr.myflat(1)); // [1, 2, [3, [4]], 5, 6](只扁平1层)
console.log(arr.myflat(2)); // [1, 2, 3, [4], 5, 6](扁平2层)
console.log(arr.myflat(0));
优点:逻辑清晰,易懂,且易于维护
缺点:当数组嵌套深度极深时,可能导致调用栈溢出
方案二:迭代实现(规避调用栈问题)
迭代算法的算法思想参考广度优先遍历(BFS) | 层序遍历
中等难度的层序遍历二叉树助力你理解\]([102. 二叉树的层序遍历 - 力扣(LeetCode)](https://link.juejin.cn?target=https%3A%2F%2Fleetcode.cn%2Fproblems%2Fbinary-tree-level-order-traversal%2Fdescription%2F "https://leetcode.cn/problems/binary-tree-level-order-traversal/description/"))
js
Array.prototype.myflat = function (depth = 1) {
//创建自己的调用栈 并遍历原数组 [{item1,depth},{item2,depth}]
const stack = [...this.map(item => ({ item, remainingDepth: depth }))];
const res = [];
//调用栈中存在数据时不断进行循环
while (stack.length > 0) {
//出栈 并解构该对象
const { item, remainingDepth } = stack.pop();
//判断 item 是否为数组 且 剩余深度 大于 0
if (Array.isArray(item) && remainingDepth > 0) {
//再次遍历出数组 并加入栈尾
stack.push(...item.map(subItem => ({
item: subItem,
remainingDepth: remainingDepth - 1
})));
} else if (this[i] !== undefined && this[i] !== null){
res.push(item);
//res.unshift(item) 注意这里用unshift()也可以
//1.对数组来说头部插入 比 尾部插入的性能开销更大
//2.这是一个栈结构,尊重先进后出
//所以避免使用 unshift()
}
}
return res.reverse();//再反转一下结果数组
}
// 测试
const testArr2 = [1, [2, [3, [4, 5]]]];
console.log(testArr2.myflat(2)); // [1, 2, 3, [4, 5]]
console.log(testArr2.myflat(Infinity)); // [1, 2, 3, 4, 5]
优点:避免了递归的栈溢出问题,支持任意深度的嵌套数组;
缺点:理解代码逻辑更加困难,需要手动管理栈结构,还需要反转数组
方案三:采用数组原生方法
注意以下方法都无法控制深度,且适用于对数组方法比较熟悉的开发者,reduce()方法无法控制调用栈,且两种方法都会频繁创建新的数组(因为concat()方法)。
- 方式一 :采用 reduce() + concat() 的方式, reduce()方法可遍历数组并累积结果,通过判断元素是否为数组,决定是直接累积还是递归处理后累积
js
Array.prototype.myflat = function () {
return this.reduce((acc, item) => {
// 若为数组,递归处理后合并;否则直接添加到累积器
return acc.concat(Array.isArray(item) ? item.myflat() : item);
}, []); // 初始累积器为空数组
};
// 测试
console.log([1, [2, [3, 4]]].myflat()); // [1, 2, 3, 4]
- 方式二 :采用 some() + concat(),some()方法可判断数组中是否存在满足条件的元素(此处判断是否有数组元素),若存在则用concat()展开一层,循环直至数组中无嵌套数组
js
Array.prototype.myflat = function () {
// 循环判断数组中是否有嵌套数组,有则展开一层
let res = this.slice()
while (res.some(item => Array.isArray(item))) {
res = [].concat(...res); // 展开一层数组
}
return res;
};
// 测试
console.log([1, [2, [3, [4, 5]]]].myflat()); // [1, 2, 3, 4, 5]
就写数组的,对象的懒得写了
三、总结
- 开发环境建议直接用 flat()
- 需兼容低版本浏览器,可使用迭代实现方案作为 Polyfill,或通过 Babel 转译。
- 处理极深嵌套数组,用迭代。
- 扁平化过程注意处理空元素。