阅读 2452

JS数组扁平化的一些方法(7-8种)

引子:这些日子在看es相关的新属性是有一个数组的方法 flat() 引起的我的注意。

1 什么是扁平化?

我去找了一下维基百科,上面并没有关于扁平化的直接解释,只有一个扁平化组织释义。

先来说一下,与扁平化对立的组织: 金字塔组织,这个众所周知,它表现的层级结构就是一个金字塔式的形状。

扁平化组织(Flat organization)也被称为横向组织(horizontal organization),是一种在员工和Boss之间很少存在或不存在中间管理层的组织。

左侧就是金字塔组织,右侧就是扁平化组织。可见扁平化组织的层级是很少的,基层里组成单位(人)是最多的。

2 什么是数组扁平化

['a','b','c'] //这是一个拥有3个元素的数组,是一个一维数组(不存在数组嵌套)。
[['a','b'],['c','d'],['e','f']] 从整体上看是一个数组,但是其中的元素又是数组,即数组中嵌套数组,这就是二维数组

以此类推·····
['a',['b',['c']]]//3维数组
['a',['b',['c',[.....]]]]//n维数组
数组扁平化就是把多维数组转化成一维数组。(可以联想上图。。。类比于数组的展开)

3 数组扁平化的方法

3.1 es6提供的新方法 flat(depth)

let a = [1,[2,3]];  
a.flat(); // [1,2,3]  
a.flat(1); //[1,2,3]
复制代码

flat(depth) 方法中的参数depth,代表展开嵌套数组的深度,默认是1

所以我们可以添加参数1,或者直接调用flat()来对2维数组进行扁平化,如果我们可以提前知道数组的维度,对这个数组进行扁平化处理,参数depth的值就是数组的维度减一。

let a = [1,[2,3,[4,[5]]]];  
a.flat(4-1); // [1,2,3,4,5]  a是4维数组
复制代码

其实还有一种更简单的办法,无需知道数组的维度,直接将目标数组变成1维数组。 depth的值设置为Infinity。

let a = [1,[2,3,[4,[5]]]];  
a.flat(Infinity); // [1,2,3,4,5]  a是4维数组
复制代码

3.2 for循环

var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
  function flatten(arr) {
    var res = [];
    for (let i = 0, length = arr.length; i < length; i++) {
      if (Array.isArray(arr[i])) {
        res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
      //res.push(...flatten(arr[i])); //扩展运算符  
      } else {
        res.push(arr[i]);
      }
    }
    return res;
  }
  flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
复制代码

利用for循环遍历数组的每一项并加以判断,如果不是数组,就执行push操作,
是数组的化,就再次执行该函数(递归),直至遍历完整个数组。
ps: ...和concat()可以进行替换,所以完全可以算是2种方法。

3.3 while循环

 var arr1 = [1, 2, [3], [1, 2, 3, [4, [2, 3, 4]]]];
 function flatten(arr) {
      while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
        //arr = Array.prototype.concat.apply([],arr);
      }
      return arr;
    }
    flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
复制代码

同理,利用while判断加上some的遍历来实现扁平化。

3.4 reduce方法

 var arr1 = [1, 2, [3], [1, 2, 3, [4, [2, 3, 4]]]];
 function flatten(arr) {
      return arr.reduce((res,next) =>{
        return res.concat(Array.isArray(next)? flatten(next) : next);
      },[]);
    }
复制代码

这里使用的是数组的reduce方法,需要注意的是reduce方法,我们传递了两个参数,
第一个参数就是就是处理扁平化的箭头函数
第二个参数是一个空数组,也是作为遍历的开始。(res)

3.5 使用 stack 无限反嵌套多层嵌套数组

  var arr1 = [1, 2, [3], [1, 2, 3, [4, [2, 3, 4]]]];
  function flatten(input) {
      const stack = [...input]; //保证不会破坏原数组
      const result = [];
      while (stack.length) {
        const first = stack.shift();
        if (Array.isArray(first)) {
          stack.unshift(...first);
        } else {
          result.push(first);
        }
      }
      return result;
    }
    flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
复制代码

这种方法其实并没有多复杂,这里没有使用递归,
原理判断stack数组长度,从数组从前往后拆出每一项(此时stack数组长度减一),
并进行判断,如果不是数组,执行push操作,该项被移除,
是数组的化,数组展开后,执行unshift操作,添加到stack数组的前面,此时stack的数组的长度会增加,这样不断的进行判断和扩展,直至每一项被拆出。
ps:
1 开始的[...input]是为了保证传进来的数组不会被破坏。
2 上面的shift(),unshift()可以换成pop()和push(),只不过return前要执行reverse()操作

3.6 如果数组的项全为数字,可以使用join(),toString()

如果数组的项全为数字,可以使用join(),toString()进行扁平化操作。

 function flatten(input) {
  return input.toString().split(',').map(item => +item);
  // return input.join().split(',').map(item => +item);
  // return input.join(',').split(',').map(item => +item);
}
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
复制代码

原理很简单,先把数组转换成字符串,这个过程会吧[]去掉,然后再调用split()方法转换成数组,最后不能忘了,吧每一项转换为数组,即调用map()方法。

ps:数组的toString()方法是定义在Array.prototype(原型)上的,会对数组每一项执行toString()
然后在拼接成一个字符串,所以即使数组是嵌套的,里面嵌套的数组也会执行toString方法。
----这里是我个人的理解。
复制代码
上面的解释也是不严谨的,肯定会有大佬说,undefinednull上并没有toString()方法,  
[null,undefined].toString();//"null,undefined"  
这里可以理解js引擎做了判断,执行了String()方法。保证数组的toString()方法不会报错。
复制代码

4 总结

数组扁平化的方法是很多种的,如果要强行使用call,apply等,可能还要在加上几种,除了flat()以外,其余的方法,都要遍历数组,进行判断,除了3.5这种方法外,基本上都要递归。
等有时间,在补充下,各个方法的性能区别,如果只是数字,建议用最后的方法。

文中若有不对的地方,请各位大佬积极指正,及时改进,共同进步。

有缘再会。。。。。。。

3-25更新

数组的es6的新方法flatMap();可以用来展开2维数组。 flatMap()方法类似先执行数组的map()(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法(展开一层)。

  var arr = [["今", "天", "天", "气", "不", "错"],[""],["早", "上", "好"]];
  arr.flatMap(item => item);
  //["今", "天", "天", "气", "不", "错", "", "早", "上", "好"]
复制代码

flatMap()与map()不同,并不会改变原数组

5 后续性能问题(3-29更新):

5.1 生成n维数组
    let arr =[];
    for(let i = 199; i> 0 ;i--){
      arr = [i].concat([arr]); //生成一个199维的数组
    }
复制代码

这里生成199维的数组,而不是更大的数组,主要有两个原因:
1、 实际项目中不会出现很深维度的数组,即使是JSON数据,也不太可能超过10维,毕竟解析起来,很繁琐。
2、 这里是为了方便测试,如果设置的维度太深,例如9999,那么有些扁平化数组的方法就会报错。

这个错误是说栈溢出。一般出现在递归调用或者死循环。所以上面采用了199维。

5.2 性能测试

不多说废话,我采用的方法是console.time()/timeEnd()来生成执行函数扁平化的时间。 结果如下:

代码不贴了,留一个 github地址 ,有兴趣的复制自测下。

结论是:
1、性能最好的是es6的falt()
2、最差的就是while() reduce()
3、剩下的方法差距都不是很大。
如果有可能支持新属性,最好使用新属性,或者stack()方法。

最后贴一下支持flat()的浏览器:

IE浏览器是不支持这个方法的。Chrome也要升级到69以上。所以使用该方法时,一定要注意这个问题。