转载: 翻译 | 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(..)
。
无论选择采用哪种方法,都需要对数组合并策略进行批判性思考,而不是想当然。