转载: 翻译 | JavaScript 小技巧之数组合并

原文链接:https://davidwalsh.name/combining-js-arrays

原译文链接:http://www.ituring.com.cn/article/497290

这是一篇介绍 JavaScript 技术的小短文。我们将会讲到组合/合并两个数组的不同策略,以及每一种方法的优缺点。

首先展示一下应用场景:

var a = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ];
var b = [ "foo", "bar", "baz", "bam", "bun", "fun" ];

很显然,拼接后的结果是这个样子滴:

[
   1, 2, 3, 4, 5, 6, 7, 8, 9,
   "foo", "bar", "baz", "bam" "bun", "fun"
]

concat(..)

最常见的做法如下:

var c = a.concat( b );

a; // [1,2,3,4,5,6,7,8,9]
b; // ["foo","bar","baz","bam","bun","fun"]

c; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

从上述代码可以看出,c 是一个由 a 和 b 两个数组合并而成的全新的数组,而 a 和 b 则不受影响。相当简单,对吧?

假如 a 和 b 分别包含 10,000 元素呢?那么 c 中就会包含 20,000 个元素,占用的内存基本上让 a 和 b 占用的内存翻倍。

“这没什么大不了的!”你微微一笑。我们可以把 a 和 b 删除嘛,这样就可以将占据的内存回收了,这样总可以吧?危机解除!

a = b = null; // `a` and `b` can go away now

哦。对于一些小数组来说,这样做当然没有问题。但是对于大数组来说,或者经常性地执行这样的操作,再或者在执行环境内存有限的情况下,这样做还远远不够。

循环插入

好吧,那使用 Array#push(..) 方法将一个数组的内容追加到另外一个数组呢:

// `b` onto `a`
for (var i=0; i < b.length; i++) {
    a.push( b[i] );
}

a; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

b = null;

现在,a 中包含的是原本 a 中的元素外加 b 中的元素。

看起来对于内存的使用有效多了。

可是假如 a 比较小而 b 相对来说很大呢?出于内存利用以及执行速度的考量,你一定希望把小数组 a 插入到 b 的前面而不是把大数组 b 追加到 a 后面。没问题,只要用 unshift(..) 替换 push(..) 然后反方向遍历就可以了:

// `a` into `b`:
for (var i=a.length-1; i >= 0; i--) {
    b.unshift( a[i] );
}

b; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

a = null;

使用函数小技巧

遗憾的是,for 循环不够优雅,也不容易维护。还有没有更好的办法呢?

下面是我们的第一次尝试,用的是 Array#reduce

// `b` onto `a`:
a = b.reduce( function(coll,item){
    coll.push( item );
    return coll;
}, a );

a; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

// or `a` into `b`:
b = a.reduceRight( function(coll,item){
    coll.unshift( item );
    return coll;
}, b );

b; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

Array#reduce(..) 与 Array#reduceRight(..) 看起来不错,只是有点笨拙。ES6 中的 => 箭头表达式可以对其进行适当“瘦身”,但是依然需要对于每一个元素进行一次函数调用,这一点有些令人遗憾。

下面的方法怎么样呢:

// `b` onto `a`:
a.push.apply( a, b );

a; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

// or `a` into `b`:
b.unshift.apply( b, a );

b; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

看起来好多了,是吧?!尤其是这里的 unshift(..) 不再需要顾及遍历顺序的问题。ES6 中的展开运算符(spread operator)会更棒:a.push( ...b ) 或是 b.unshift( ...a )

但是呢,公主与王子并没有从此过上幸福无虞的生活。在两种情况下,将 a 或 b 传给 apply(..) 第二个参数(或者通过 ... )展开运算符意味着数组需要展开为函数的参数。

第一个主要问题在于,我们将要追加的数组的元素数量翻倍了(当然是临时性的),因为实质上要将数组内容拷贝到函数调用栈上。另外,不同的 JS 引擎因实现方式的不同,对于可以传入函数的参数的数量限制也不尽相同。

所以,假如要追加的数组中有一百万个元素,那么几乎一定会超过函数 push(..) 和 unshift(..) 的调用栈限制的大小。嗯!几千元素应该是没有问题的,不过需要小心设定一个合理的安全上限。

注意: 你可以尝试使用 splice(..),但是结论与 push(..) / unshift(..) 相同。

一个可行的方式是,依然采用上述方法,将数组划分为处于安全范围的片段,进行批处理:

function combineInto(a,b) {
    var len = a.length;
    for (var i=0; i < len; i=i+5000) {
        b.unshift.apply( b, a.slice( i, i+5000 ) );
    }
}

且慢,接下来我们要回到可读性(或者还有执行效率)的老话题了。我们还是在抛弃当前所获得的所有有效方式之前就此打住吧。

总结

Array#concat(..) 是合并两个(甚至多个)数组的行之有效的方法。但是隐含的风险是,它直接创建了一个新的数组,而不是在原来数组的基础上进行修改。

在原来数组的基础上进行修改有多种可行的方式,但均有某种程度的妥协。

从不同方法(包括未在这里展示的方法)的优缺点来看,或许最好的方法就是 reduce(..) 和 reduceRight(..)

无论选择采用哪种方法,都需要对数组合并策略进行批判性思考,而不是想当然。

posted @ 2017-10-20 00:05  Lawliet__zmz  阅读(150)  评论(0编辑  收藏  举报