深入浅出:ES5/ES6+数组扁平化详解

1.前言

我们为什么需要知道数组实现方法中的原理?拿来会用只是基础,当有问题出现的时候,如果你知道原理,那么问题就容易定位。比如接下来要谈的是数字的扁平化方法,在ES2019 中增加了数组的扁平化方法Array.prototype.flat。如果你手动去实现这个方法,可以学习到数组的处理逻辑递归与迭代的区别以及它们的实际用法数据结构的实际应用

2.详解

数组的扁平化通俗来说,是将嵌套的数组结构从多维降到某维。 Array.prototype.flat 具备一个可选参数是depth,默认值是 1,可以灵活指定需要扁平的层级,返回一个新的数组,对原数组进行浅拷贝。

ini 复制代码
//depth默认是 1
const arr1 = [1, 2, [3, 4, [5, 6]]];
arr1.flat();
// [1, 2, 3, 4, [5, 6]]

const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat(2);
// [1, 2, 3, 4, 5, 6]

还有手动实现数组扁平化:

  • ES5实现扁平化:for 循环(forEach遍历)+ 递归
  • ES6+实现扁平化:reduce版本 + 递归
  • ES6+实现扁平化:some 方法 + 扩展运算符
  • ES6+实现扁平化:迭代 + 数据结构栈

3.适用场景

Array.prototype.flat日常开发中的首选方案,但是也有一些特别情况不适用:

  1. 兼容旧老浏览器,可以使用 ES5 实现或者进行转译
  2. 在扁平化的基础上定制处理特殊的逻辑,可以使用 reduce 版本或者 Array.prototype.flatMap方法
  3. 处理更深层次的数据,避免递归栈溢出,可以使用迭代版本栈结构的方式

4.优缺点

  1. Array.prototype.flat
  • 优点:简洁性能效率性能高,远超手动实现,推荐使用
  • 缺点:兼容性不佳、无法处理定制逻辑、更深层次数据可能递归栈溢出
  1. for 循环(forEach遍历)+ 递归
  • 优点:ES5实现兼容性好
  • 缺点:性能不如原生实现的高
  1. reduce版本
  • 优点: 可以处理定制逻辑,过滤一些条件
  • 缺点: 性能不如原生实现的高
  1. 迭代 + 数据结构栈
  • 优点:处理更深层次的数据,避免递归栈溢出
  • 缺点: 性能不如原生实现的高

5.代码实现

5.1 ES5实现方式 forEach遍历 + 递归

  1. 声明一个 function,参数一:需要扁平化的数组,参数二:支持的扁平化的深度 depth
javascript 复制代码
function flatten(arr,depth ) {}
  1. 对参数的类型进行判断,参数一是数组、参数二 depth 是非负整数,depth默认值是 1
javascript 复制代码
funxction flatten (arr,depth) {

    depth = depth === undefined?1:depth

    if(!Array.is(arr)) {
        throw new TypeError("第一个参数必须是数组类型")
    } else if(typeof depth !== "number" || depth < 0 || Math.floor(depth) !== depth) {
        throw new TypeError("第二个参数必须是非负整数")
    }

    //如果 depth 是 0,则返回对原数组进行浅拷贝的新数组
    if(depth === 0) return arr.slice()

    
}
  1. 对数组子元素进行判断如果为数组,那么进行递归处理,不是数组就进入新数组中保存起来
scss 复制代码
function flatten(arr,depth){
    ...

    var reault = []
    function flated(element){
        if(Array.isArray(item)) {
            element.forEach(function(item){
                flated(item)
            })
        } else {
            reault.push(item)
        }
        
    }
    flated(arr)
    return result
}
  1. 支持灵活设置深度,既要每个递归层级深度逐渐递增,也要保留每一层级的当前 depth 值,所以需要注意递增深度值使用curDepth + 1,而不是curDepth++
scss 复制代码
function flatten(arr,depth){
    ...

    var reault = []
    function flated(element,curDepth){
        if(curDepth > depth){
            result.push(element)
            return result
        }
        if(Array.isArray(element)) {
            element.forEach(item => {
                flated(item, curDepth + 1)
            })
        } else {
            reault.push(element)
        }
    }
    flated(arr,0)
    return result
}

5.全部实现代码

javascript 复制代码
function flatten (arr,depth) {

    depth = depth === undefined?1:depth

    if(!Array.isArray(arr)) {

        throw new TypeError('第一个参数必须是数组');

    } else if (typeof depth !== "number" || depth < 0 || Math.floor(depth) !== depth) {

        throw new TypeError("第二个参数必须是非负整数")
    }

    //如果 depth 是 0,则返回对原数组进行浅拷贝的新数组
    if(depth === 0) return arr.slice()

    const result = []

    function flated (element,curDepth) {

        if(curDepth > depth) {
            result.push(element)
            return result
        }

        if(Array.isArray(element)) {
            element.forEach(item => {
                flated(item, curDepth + 1)
            })
        } else {
            arr.push(element)
        }

    }

    flated(arr,0)
    return result
}

module.exports = flatten;

5.2 reduce实现扁平化版本

  1. 核心是会使用reduce 方法,数组的reduce 方法,具备两个参数,
  • 参数一回调函数callbackFn:为每个数组元素执行的函数,其返回值是下一次执行回调函数的中的accumulator参数的值,如果是最后一次调用,那么就会作为 reduce()方法的返回值。callbackFn具备三个参数
    • accumulator:上一次调用 callbackFn 的结果。在第一次调用时,如果指定了 initialarr 则为指定的值,否则为 array[0] 的值。
    • currentarr: 当前元素的值。在第一次调用时,如果指定了 initialarr,则为 array[0]的值,否则为 array[1]
    • currentIndex: currentarr 在数组中的索引位置。在第一次调用时,如果指定了 initialarr 则为 0,否则为 1。
  • 参数二是初始值initialarr,第一次调用回调函数callbackFn时初始化accumulator的值,是个可选参数,如果传了,则回调函数callbackFn从第一个元素开始执行,如果不传,那么初始值就默认为第一个元素,回调函数callbackFn从第二个元素开始执行。
javascript 复制代码
//核心代码,其他逻辑与上面5.1一致
const flated = (element,curDepth) => {

    if(curDepth >= depth) return element

    return element.reduce((acl,curVal) => {

        if(Array.isArray(curVal)) {
            return acl.concat(flated(curVal,curDepth + 1))
        } else {
            return acl.concat(curVal)
        }
    },[])
}
  1. 实现完整的代码
javascript 复制代码
const flatten = (arr,depth = 1) => {

    if(!Array.isArray(arr)) {

        throw new TypeError('第一个参数必须是数组');

    } else if (typeof depth !== "number" || depth < 0 || Math.floor(depth) !== depth) {

        throw new TypeError("第二个参数必须是非负整数")
    }

    if(depth === 0) return arr.slice()


    const flated = (element,curDepth) => {

        if(curDepth >= depth) return element

        return element.reduce((acl,curVal) => Array.isArray(curVal)?acl.concat(flated(curVal,curDepth + 1)):acl.concat(curVal),[])
    }

    return flated(arr, 0)
     
}

5.3 扩展运算符实现扁平化

  1. 核心是采用 some 方法,通过循环遍历判断是否是嵌套结构,如果是则继续展开
javascript 复制代码
const flatten = (arr,depth = 1) => {

    if(!Array.isArray(arr)) {

        throw new TypeError('第一个参数必须是数组');

    } else if (typeof depth !== "number" || depth < 0 || Math.floor(depth) !== depth) {

        throw new TypeError("第二个参数必须是非负整数")
    }

    if(depth === 0) return arr.slice()

    let result = [...arr]

    while(result.some(item => Array.isArray(item))) {
        result = [].concat(...result)
    }

    return result
     
}

5.4 迭代 + 栈结构实现扁平化

  1. 核心点需要满足栈的特点,后进先出,直到栈中为空,则停止循环遍历
c 复制代码
const faltten = (arr,depth) => {
    ...
    //记录栈中的每个元素当前的深度,初始值为 0
    const stack = arr.map(item => [item,0])

    //记录最终结果
    const result = []
    while(stack.length) {
        //先取出栈中最后进入的值和当前 depth
        const [currentVal,curDepth] = stack.pop()

        if(Array.isArray(currentVal) && curDepth < depth) {

            stack.push(...currentVal.map(item => [item,curDepth + 1]))

        } else {
            result.unshift(currentVal)
        }
    }
    return result
}
  1. 完整代码
javascript 复制代码
const faltten = (arr,depth) => {
    
    if(!Array.isArray(arr)) {

        throw new TypeError('第一个参数必须是数组');

    } else if (typeof depth !== "number" || depth < 0 || Math.floor(depth) !== depth) {

        throw new TypeError("第二个参数必须是非负整数")
    }

    if(depth === 0) return arr.slice()

    //记录栈中的每个元素当前的深度,初始值为 0
    const stack = arr.map(item => [item,0])

    //记录最终结果
    const result = []
    while(stack.length) {
        //先取出栈中最后进入的值和当前 depth
        const [currentVal,curDepth] = stack.pop()

        if(Array.isArray(currentVal) && curDepth < depth) {

            stack.push(...currentVal.map(item => [item,curDepth + 1]))

        } else {
            result.unshift(currentVal)
        }
    }
    return result
}

6.测试验证

单元测试,使用了Jest框架,下面是测试的几个测试用例,完全通过

  1. 5.1/5.2/5.4版本,能够支持灵活设置 depth 的测试用例是相同的一套
scss 复制代码
const flatten = require('../xxxx.js'); 

describe('数组扁平化函数测试', () => {
  // 基础功能测试
  test('不指定深度时默认展开1层', () => {
    const arr = [1, [2, 3], [4, [5]]];
    expect(flatten(arr)).toEqual([1, 2, 3, 4, [5]]);
  });

  test('指定深度为1时展开1层', () => {
    const arr = [1, [2, [3]], 4];
    expect(flatten(arr, 1)).toEqual([1, 2, [3], 4]);
  }); 

  test('指定深度为2时展开2层', () => {
    const arr = [1, [2, [3, [4]]], 5];
    expect(flatten(arr, 2)).toEqual([1, 2, 3, [4], 5]);
  });

  // 边界情况测试
  test('深度为0时不展开任何层级', () => {
    const arr = [1, [2, [3]]];
    expect(flatten(arr, 0)).toEqual([1, [2, [3]]]);
  });

  test('深度大于实际嵌套层级时完全展开', () => {
    const arr = [1, [2, [3]]];
    expect(flatten(arr, 10)).toEqual([1, 2, 3]);
  });

  // 特殊数组测试
  test('空数组返回空数组', () => {
    expect(flatten([])).toEqual([]);
  });

  test('非嵌套数组不变化', () => {
    const arr = [1, 2, 3];
    expect(flatten(arr, 5)).toEqual([1, 2, 3]);
  });

  test('包含空数组的情况', () => {
    const arr = [1, [], [2, [[]]], 3];
    expect(flatten(arr, 2)).toEqual([1, 2, [], 3]);
  });

  // 包含不同类型元素的数组
  test('包含多种数据类型的数组', () => {
    const arr = [      'a',      [1, true],
      [null, [undefined, { key: 'arr' }]]
    ];
    expect(flatten(arr, 2)).toEqual([
      'a', 1, true, null, undefined, { key: 'arr' }
    ]);
  });

  // 异常处理测试
  test('非数组参数抛出错误', () => {
    expect(() => flatten('not an array')).toThrow(TypeError);
  });

  test('非数字深度参数抛出错误', () => {
    expect(() => flatten([1, 2], 'deep')).toThrow(TypeError);
  });

  test('负数深度参数抛出错误', () => {
    expect(() => flatten([1, 2], -1)).toThrow(TypeError);
  });
});
    
  1. 5.3扩展运算符实现版本,只能支持完全展开,不支持灵活设置深度,下面是它的测试用例
scss 复制代码
const flatten = require('../xxx'); 

describe('数组扁平化函数测试', () => {
  // 基础功能测试
  test('深度大于实际嵌套层级时完全展开', () => {
    const arr = [1, [2, [3]]];
    expect(flatten(arr)).toEqual([1, 2, 3]);
  });

  // 特殊数组测试
  test('空数组返回空数组', () => {
    expect(flatten([])).toEqual([]);
  });

  test('非嵌套数组不变化', () => {
    const arr = [1, 2, 3];
    expect(flatten(arr)).toEqual([1, 2, 3]);
  });

  test('包含空数组的情况', () => {
    const arr = [1, [], [2, [[]]], 3];
    expect(flatten(arr)).toEqual([1, 2, 3]);
  });

  // 包含不同类型元素的数组
  test('包含多种数据类型的数组', () => {
    const arr = [      'a',      [1, true],
      [null, [undefined, { key: 'value' }]]
    ];
    expect(flatten(arr)).toEqual([
      'a', 1, true, null, undefined, { key: 'value' }
    ]);
  });

  // 异常处理测试
  test('非数组参数抛出错误', () => {
    expect(() => flatten('not an array')).toThrow(TypeError);
  });
});
    

7.总结

在日常开发中,还是推荐使用原生的Array.prototype.flat方法,它是由JavaScript 引擎(如 V8)底层实现,经过高度优化,在大多数场景下还是优于手动实现的递归和迭代的方法。 但是在一些特殊的场景,我们也可以进行手动实现,比如对兼容性要求比较高,需要处理一些定制化的逻辑,以及数据层级很深,那么我们就可以根据实际情况去判断使用哪种方法更为合适。

相关推荐
zzywxc78711 分钟前
详细探讨AI在金融、医疗、教育和制造业四大领域的具体落地案例,并通过代码、流程图、Prompt示例和图表等方式展示这些应用的实际效果。
开发语言·javascript·人工智能·深度学习·金融·prompt·流程图
大明8811 分钟前
用 mouseover/mouseout 事件代理模拟 mouseenter/mouseleave
前端·javascript
林太白18 分钟前
Nuxt.js搭建一个官网如何简单
前端·javascript·后端
晴空雨19 分钟前
一个符号让 indexOf 判断更优雅!JavaScript 位运算的隐藏技巧
前端·javascript
浮灯Foden24 分钟前
算法-每日一题(DAY13)两数之和
开发语言·数据结构·c++·算法·leetcode·面试·散列表
前端snow32 分钟前
前端无接口实现Table导出Excel的两种方案(附完整代码)
javascript·vue.js·react.js
古夕1 小时前
my-first-ai-web_问题记录03——NextJS 项目框架基础扫盲
前端·javascript·react.js
曲意已决2 小时前
《深入源码理解webpac构建流程》
前端·javascript
CC__xy2 小时前
04 类型别名type + 检测数据类型(typeof+instanceof) + 空安全+剩余和展开(运算符 ...)简单类型和复杂类型 + 模块化
开发语言·javascript·harmonyos·鸿蒙