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. 扁平化过程注意处理空元素。
相关推荐
CC码码19 小时前
前端字符串排序搜索可以更加细化了
前端·javascript·面试
Crystal32819 小时前
冒泡排序 bubble sort
前端·javascript·面试
阿蓝灬20 小时前
clientWidth vs offsetWidth
前端·javascript
用户904438163246020 小时前
从40亿设备漏洞到AI浏览器:藏在浏览器底层的3个“隐形”原理
前端·javascript·浏览器
鸡吃丸子20 小时前
React Native入门详解
开发语言·前端·javascript·react native·react.js
阿蒙Amon20 小时前
JavaScript学习笔记:12.类
javascript·笔记·学习
阿蒙Amon21 小时前
JavaScript学习笔记:10.集合
javascript·笔记·学习
馬致远21 小时前
Vue TodoList 待办事项小案例(代码版)
前端·javascript·vue.js
一字白首1 天前
Vue 进阶,Vuex 核心概念 + 项目打包发布配置全解析
前端·javascript·vue.js
栀秋6661 天前
从前端送花说起:HTML敲击乐与JavaScript代理模式的浪漫邂逅
前端·javascript·css