本文深入探讨JavaScript中合并数组的多种实现方式,包括concat、循环push/unshift、reduce以及apply技巧。重点分析各方法在内存占用、执行效率及调用栈限制方面的差异,并提供处理大数组时的分段合并策略,帮助开发者根据实际场景选择最优方案。
JavaScript数组合并概述
本文介绍几种在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方法及其内存开销
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包含10,000个元素,数组b也有1万个元素,合并后的c将拥有2万个元素,导致内存使用量暂时翻倍。
有人可能会说,让垃圾回收机制处理,将a和b设为null即可解决问题:
代码如下:
a = b = null; // 'a'和'b'就被回收了对于元素较少的小数组,这确实不是问题。但在内存受限的系统或需要频繁执行此操作的场景中,这种内存翻倍的现象仍有优化空间。
循环插入与push/unshift技巧
循环插入
我们可以尝试将数组b的内容逐个复制到数组a中,使用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包含了数组b的内容。
从内存角度看,这种方式似乎更优。
但如果数组a较小,为了节省内存和提高速度,你可能希望将较小的数组a插入到数组b的前面。只需将push(..)替换为unshift(..)即可:
代码如下:
// `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"]
函数式方法与apply技巧
功能技巧
虽然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操作符写法更为优雅:a.push( ...b ) 或 b.unshift( ...a )。
处理大数组的长度限制与分段策略
数组最大长度限制
上述方法面临的主要问题是内存增长(尽管是暂时的),因为元素是通过函数调用复制到调用栈中的。此外,不同的JavaScript引擎对拷贝数据的长度有限制。如果数组包含一百万个元素,很可能会超出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 ) );
}
}
这种写法虽然牺牲了部分可读性,但能有效避免栈溢出风险,尽管代码看起来可能变得复杂。