JS: 实现扁平化函数 flat

在 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 () 的局限性

  1. 浏览器兼容问题:作为 ES2019的新特性,部分的旧版本浏览器和IE浏览器不支持该方法。
  2. 只能处理数组嵌套,无法对扁平化过程中的元素进行自定义的处理(比如筛选一些元素)。
  3. 不能根据元素类型动态的调整扁平化逻辑。(比如无法处理对象)

正是因为这些局限性,促使作者写这一篇文章进行学习。

二、实现一份扁平化函数(仅数组)

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]

就写数组的,对象的懒得写了

三、总结

  1. 开发环境建议直接用 flat()
  2. 需兼容低版本浏览器,可使用迭代实现方案作为 Polyfill,或通过 Babel 转译。
  3. 处理极深嵌套数组,用迭代。
  4. 扁平化过程注意处理空元素。
相关推荐
剽悍一小兔4 小时前
小程序到底用Store还是LocalStorage ?
javascript
一只小风华~5 小时前
学习笔记:Vue Router 中的链接匹配机制与样式控制
前端·javascript·vue.js·笔记·学习·ecmascript
uhakadotcom5 小时前
在chrome浏览器插件之中,options.html和options.js常用来做什么事情
前端·javascript·面试
西瓜树枝5 小时前
Chrome 扩展开发从入门到实践:以 Cookie 跨页提取工具为例,拆解核心模块与交互逻辑
前端·javascript·chrome
冰糖雪梨dd5 小时前
JS中new的过程发生了什么
开发语言·javascript·原型模式
小帆聊前端6 小时前
Lodash 深度解读:前端数据处理的效率利器,从用法到原理全拆解
前端·javascript
一枚前端小能手6 小时前
🔍 那些不为人知但是好用的JS小秘密
前端·javascript
北城以北88887 小时前
Vue-- Axios 交互(二)
javascript·vue.js·交互
Zuckjet_7 小时前
第 7 篇:交互的乐趣 - 响应用户输入
前端·javascript·webgl