如何将另一个数组添加到现有的JavaScript数组中,而不创建新数组

1444

似乎没有一种方法可以将一个现有的 JavaScript 数组与另一个数组扩展,也就是模仿 Python 的 extend 方法。

我想要实现以下内容:

>>> a = [1, 2]
[1, 2]
>>> b = [3, 4, 5]
[3, 4, 5]
>>> SOMETHING HERE
>>> a
[1, 2, 3, 4, 5]
我知道有一个方法,但它会创建一个新的数组而不是简单地扩展第一个数组。我想要一种算法,在ab大很多时仍然能高效工作(即不复制a)。 注意: 这不是如何向数组附加内容?的重复问题--这里的目标是将一个数组的所有内容添加到另一个数组中,并且要“原地”执行此操作,即不复制扩展数组的所有元素。

10
来自@Toothbrush对一个回答的评论:“a.push(...b)”。它在概念上与最佳答案类似,但针对ES6进行了更新。 - tscizzle
a.push(...b)
- Ali NajafZadeh
20个回答

2120

.push 方法可以接受多个参数。您可以使用spread operator将第二个数组的所有元素作为参数传递给 .push

>>> a.push(...b)

如果你的浏览器不支持ECMAScript 6,你可以使用.apply代替:

>>> a.push.apply(a, b)

或者,如果你认为更清晰:

>>> Array.prototype.push.apply(a,b)

请注意,如果数组b太长(在浏览器上大约100,000个元素开始出问题),所有这些解决方案都会因堆栈溢出错误而失败。
如果您无法保证b足够短,则应使用另一个答案中描述的标准基于循环的技术。

3
我认为这是你最佳的选择。其他任何方案都需要迭代或再次运用 apply() 函数。 - Peter Bailey
70
如果“b”(需要扩展的数组)很大(根据我的测试,在Chrome浏览器中,大约> 150000个条目),则此答案将失败。你应该使用for循环,或者更好地使用内置的“forEach”函数对“b”进行操作。请参考我的回答:https://dev59.com/VnM_5IYBdhLWcg3wcSx_#17368101 - jcdude
43
Array的push方法可以接收任意数量的参数,并将它们推入数组的末尾。所以,a.push('x', 'y', 'z')是一个有效的调用,它会将数组a扩展3个元素。apply是任何函数的方法,它接收一个数组,并将其元素作为显式位置元素传递给该函数。因此,a.push.apply(a, ['x', 'y', 'z'])也会将数组扩展3个元素。此外,apply方法将上下文作为第一个参数(我们再次传递a以附加到a)。 - Dzinx
3
另一个稍微容易理解一些(并且长度不长)的调用方式是:[].push.apply(a, b) - niry
@user3701996:不是这样的,我刚在Chrome 63上检查了所有变体。 - Dzinx
显示剩余4条评论

355

更新2018年: 我的新答案更好: a.push(...b)。不要再赞这个答案了,因为它实际上没有回答问题,而只是在2015年通过谷歌搜索的方式绕过了问题 :)


对于那些仅仅搜索“JavaScript数组扩展”并到达此处的人,你可以使用Array.concat

var a = [1, 2, 3];
a = a.concat([5, 4, 3]);

Concat会返回一个新数组的副本,这与主题帖作者所期望的不同。但你可能并不关心(对于大多数情况下,这应该是可以接受的)。


此外,在ECMAScript 6中还有一些很好的语法糖,以展开运算符的形式出现:

const a = [1, 2, 3];
const b = [...a, 5, 4, 3];

(它也会复制。)


75
问题明确地表明:"不创建新数组?" - Wilt
12
@Wilt:这是真的,答案解释了为什么。这已经是很久以前在谷歌上的第一个搜索结果了。此外,其他解决方案都很丑陋,想要一些惯用语和不错的东西。虽然现在arr.push(...arr2)更加新颖和更好,而且从技术上讲是回答这个问题的正确答案。 - odinho - Velmont
10
我明白你的意思,但我搜索了“javascript append array without creating a new array”,很遗憾看到得票率高达60倍的答案并没有回答实际问题。 ;) 我没有给你的回答投反对票,因为你在回答中已经说明了。 - Wilt
3
@Wilt和我搜索了"js extend array with array",然后来到了这里,谢谢:) - Davide

237

你应该使用基于循环的技术。本页面上其他基于使用.apply的答案可能对于大型数组会失败。

一个相当简洁的基于循环的实现是:

Array.prototype.extend = function (other_array) {
    /* You should include a test to check whether other_array really is an array */
    other_array.forEach(function(v) {this.push(v)}, this);
}

您随后可以执行以下操作:

var a = [1,2,3];
var b = [5,4,3];
a.extend(b);

DzinX的回答(使用push.apply方法)和其他基于.apply的方法在向数组追加大量数据时会失败(通过测试我发现,对于Chrome浏览器而言,大约在 >15万条记录时出现问题;对于Firefox浏览器而言,在 >50万条记录时出现问题)。您可以在此 jsperf中查看此错误。

出错是因为当将一个大数组作为第二个参数调用'Function.prototype.apply'时超过了调用栈大小。 (MDN关于使用Function.prototype.apply超出调用栈大小的危险的注释-请参见标题为“apply and built-in functions”的部分。)

要进行速度比较,请参阅此 jsperf(感谢EaterOfCode)。 基于循环的实现与使用Array.push.apply类似,但往往稍微慢一点比Array.slice.apply慢。

有趣的是,如果您要追加的数组是稀疏的,则上面的forEach方法可以利用该稀疏性并优于.apply的方法; 如果您想测试这一点,请查看此 jsperf

顺便说一句,不要尝试(就像我一样!)将forEach实现进一步缩短为:

Array.prototype.extend = function (array) {
    array.forEach(this.push, this);
}

为什么会得到垃圾结果呢?因为Array.prototype.forEach为它所调用的函数提供了三个参数——它们是:(元素值,元素索引,源数组)。如果你使用"forEach(this.push, this)",那么每次迭代时,所有这些参数都会被推入你的第一个数组中!


1
请注意,要测试 other_array 是否为数组,请选择此处描述的选项之一:https://dev59.com/K3RA5IYBdhLWcg3w9izq - jcdude
7
好的回答。我之前并不知道这个问题。我在这里引用了你的答案:https://dev59.com/kW855IYBdhLWcg3ww3Wa#4156156 - Tim Down
2
在V8中,“.push.apply”实际上比“.forEach”快得多,最快的仍然是内联循环。 - Benjamin Gruenbaum
1
@BenjaminGruenbaum - 你能发一下链接展示一下吗?就我所看到的,从这条评论末尾链接的jsperf收集的结果来看,在Chrome / Chromium中使用.forEach.push.apply更快(在v25之后的所有版本中)。我无法单独测试v8,但如果您有,请提供您的结果链接。请参见jsperf:http://jsperf.com/array-extending-push-vs-concat/5 - jcdude
1
@jcdude,你的jsperf无效。因为数组中的所有元素都是“undefined”,所以.forEach会跳过它们,使其成为最快的方法。.splice实际上是最快的?!http://jsperf.com/array-extending-push-vs-concat/18 - EaterOfCode
显示剩余3条评论

186

我最近感觉最优雅的是:

arr1.push(...arr2);

MDN关于扩展运算符的文章提到了在ES2015(ES6)中一种优雅的简写方式:

A better push

Example: push is often used to push an array to the end of an existing array. In ES5 this is often done as:

var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
// Append all items from arr2 onto arr1
Array.prototype.push.apply(arr1, arr2);

In ES6 with spread this becomes:

var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
请注意,arr2 的长度不能太大(最好不要超过100,000个元素),否则会导致调用栈溢出,就像jcdude的回答中所说的那样。

2
刚刚犯了一个错误,提醒大家一下。ES6版本中的arr1.push(arr2)非常接近,这可能会导致问题,例如arr1 = []; arr2=['a', 'b', 'd']; arr1.push(arr2)的结果是一个数组的数组arr1 == [['a','b','d']]而不是两个数组合并。这是一个容易犯的错误。出于这个原因,我更喜欢你下面的第二个答案。https://dev59.com/VnM_5IYBdhLWcg3wcSx_#31521404 - Seph Reed
1
使用.concat的便利之处在于第二个参数不必是一个数组。这可以通过将扩展运算符与concat结合使用来实现,例如arr1.push(...[].concat(arrayOrSingleItem)) - Steven Pribilinskiy
这是现代、优雅的 JavaScript,适用于目标数组不为空的情况。 - timbo
这个速度够快吗? - Shift 'n Tab

89

概述

  • a.push(...b) - 有限制,快速,现代语法
  • a.push.apply(a, b) - 有限制,快速
  • a = a.concat(b) 无限制,如果a很大则较慢
  • for (let i in b) { a.push(b[i]); } - 无限制,如果b很大则较慢

每个代码片段都将a修改为扩展了b的值。

"有限制"的代码片段将每个数组元素作为参数传递,函数可以传递的最大参数数量是有限制的。从该链接中可以看出,a.push(...b)b的元素数达到约32k时是可靠的(a的大小不重要)。

相关MDN文档:展开语法.apply().concat().push()

速度考虑

如果ab都很小,则每种方法都很快。因此,在大多数Web应用程序中,您可以使用push(...b)并完成它。

如果要处理的元素超过几千个,则取决于情况:

  • 将少量元素添加到大型数组中
    push(...b)非常快
  • 将许多元素添加到大型数组中
    concat略快于循环
  • 将许多元素添加到小型数组中
    concat比循环快得多
  • 通常只向任何大小的数组添加少量元素
    → 对于小的添加,循环与有限制的方法速度相当,但即使添加了许多元素也不会抛出异常,尽管可能不是最有效的方式
  • 编写一个包装函数以始终获得最大性能
    → 您需要动态检查输入的长度并选择正确的方法,可能在循环中调用push(...b_part)(使用大的b的切片)。
这让我感到惊讶:我以为a=a.concat(b)可以很好地将b复制到a中,而不必像a.push(...b)那样进行单独的扩展操作,因此始终是最快的。但事实上,尤其是当a很大时,a.push(...b)要快得多。
在Linux上使用Firefox 88测量了不同方法的速度:
a = [];
for (let i = 0; i < Asize; i++){
  a.push(i);
}
b = [];
for (let i = 0; i < Bsize; i++){
  b.push({something: i});
}
t=performance.now();
// Code to test
console.log(performance.now() - t);

参数和结果:

 ms | Asize | Bsize | code
----+-------+-------+------------------------------
 ~0 |  any  |  any  | a.push(...b)
 ~0 |  any  |  any  | a.push.apply(a, b)
480 |   10M |    50 | a = a.concat(b)
  0 |   10M |    50 | for (let i in b) a.push(b[i])
506 |   10M |  500k | a = a.concat(b)
882 |   10M |  500k | for (let i in b) a.push(b[i])
 11 |    10 |  500k | a = a.concat(b)
851 |    10 |  500k | for (let i in b) a.push(b[i])

请注意,我的系统上所有方法接受的最大值为500,000,这就是为什么BsizeAsize小的原因。
所有测试都运行了多次,以确定结果是否为异常值或代表性。快速方法在使用performance.now()进行一次运行时几乎无法测量,但由于慢速方法非常明显,而两种快速方法都采用相同的原理,因此我们不需要重复多次以微调差别。
如果数组较大,则concat方法始终很慢,但是如果循环必须执行许多函数调用并且不关心a的大小,则循环只是慢。因此,对于小的b,循环类似于push(...b)push.apply,但如果它变得很大,它不会中断;但是,当您接近极限时,concat再次变得稍快一些。

48

首先简单介绍一下JavaScript中的apply()方法,以帮助理解我们为什么使用它:

apply()方法调用一个函数,可以给定函数调用时的this值和作为数组提供的参数。

push方法是用于向数组添加元素的,但需要传入一个元素列表作为参数。而apply()方法则将函数调用所需的参数作为一个数组传递。这使我们可以轻松地使用内置的push()方法将一个数组的元素推入到另一个数组中。

假设你有以下这些数组:

var a = [1, 2, 3, 4];
var b = [5, 6, 7];

只需这样做:

Array.prototype.push.apply(a, b);

结果将是:

a = [1, 2, 3, 4, 5, 6, 7];

使用ES6中的展开运算符("...")可以完成同样的操作,像这样:

a.push(...b); //a = [1, 2, 3, 4, 5, 6, 7]; 

目前在所有浏览器上都不完全支持,但更短更好。

另外,如果您想将数组b中的所有内容 移动 到数组a中,并在此过程中清空b,您可以执行以下操作:

while(b.length) {
  a.push(b.shift());
} 

结果将如下所示:

a = [1, 2, 3, 4, 5, 6, 7];
b = [];

2
你是说将数组 b 中的元素全部取出并依次添加到数组 a 的末尾,直到数组 b 为空为止,对吗? - LarsW

39
如果您想使用jQuery,可以使用$.merge()函数。
示例:
a = [1, 2];
b = [3, 4, 5];
$.merge(a,b);

结果:a = [1, 2, 3, 4, 5]


1
我该如何找到jQuery.merge()的实现(在源代码中)? - ma11hew28
1
只花了我10分钟——jQuery是开源的,源代码发布在Github上。我搜索了“merge”,然后在core.js文件中找到了合并函数的定义,具体请参见:https://github.com/jquery/jquery/blob/master/src/core.js#L331 - Armen Michaeli
不要将其与合并排序列表的操作混淆,例如http://clhs.lisp.se/Body/f_merge.htm。 - Justin Meiners

18

正如得到最高票的回答所说,a.push(...b) 可能是正确的答案,考虑到大小限制问题。

另一方面,一些关于性能的回答似乎过时了。

以下数字为2022-05-20:

enter image description here

enter image description here

enter image description here

来自这里

看起来在 2022 年, push 在整体上都是最快的。但这可能会在未来发生改变。

忽略问题(生成新数组)的回答是没什么意义的。许多代码可能需要 / 想要就地修改数组,因为可能会有对同一数组的其他引用。

let a = [1, 2, 3];
let b = [4, 5, 6];
let c = a;
a = a.concat(b);    // a and c are no longer referencing the same array

其他引用可能深藏于某个对象中,或被捕获在闭包中等等...

作为一种可能不太好的设计,但是可以作为一个例子,想象你有一个

const carts = [
  { userId: 123, cart: [item1, item2], },
  { userId: 456, cart: [item1, item2, item3], },
];

以及一个函数

function getCartForUser(userId) {
  return customers.find(c => c.userId === userId);
}

然后您想将物品添加到购物车中

const cart = getCartForUser(userId);
if (cart) {
  cart.concat(newItems);      // FAIL 
  cart.push(...newItems);     // Success! 
}

顺带一提,建议修改 Array.prototype 的答案可能是错误的建议。更改本机原型基本上就像在代码中设置地雷一样。另一种实现可能与您的实现不同,因此它将破坏您的代码或者您会因期望其它行为而破坏其它代码。这包括当本机实现添加与您的实现冲突时。您可能会说“我知道我在使用什么,所以没有问题”,这在目前可能是真的,您只是单个开发人员,但是增加第二个开发人员后,他们无法读取您的思想。并且,当您忘记并在页面上嵌入其他库(分析?日志记录?…)并忘记留在代码中的地雷时,几年后您就成了那个第二个开发人员。
这不仅仅是理论。有无数的网络故事讲述了人们遇到这些地雷的经历。
可以说,修改本机对象的原型只有很少几种安全用途。其中之一是在旧浏览器中填充现有和指定的实现。在这种情况下,规范被定义,实现在新浏览器中正在运行,您只需在旧浏览器中获得相同的行为即可。那非常安全。在规范尚未发布但尚未运行时进行预修补理论上不安全,因为规范可能会在发布前更改。

18

我喜欢上面描述的 a.push.apply(a, b) 方法,如果你想的话,你总是可以创建一个类似这样的库函数:

Array.prototype.append = function(array)
{
    this.push.apply(this, array)
}

然后像这样使用它

a = [1,2]
b = [3,4]

a.append(b)

7
不应使用push.apply方法,因为如果您的“array”参数是大数组(例如在Chrome中>〜150000个条目),它可能会导致堆栈溢出(从而失败)。您应该使用“array.forEach”,请参见我的回答:https://dev59.com/VnM_5IYBdhLWcg3wcSx_#17368101 - jcdude
在ES6中,只需使用a.push(...b) - ΔO 'delta zero'

14

可以使用 splice() 来实现:

b.unshift(b.length)
b.unshift(a.length)
Array.prototype.splice.apply(a,b) 
b.shift() // Restore b
b.shift() // 

但是尽管它更丑陋,但至少在Firefox 3.0中不比push.apply快。


9
我发现了同样的事情,splice方法在处理长度不超过一万个元素的数组时并不能提高性能,相比之下使用push逐个添加元素反而更快。这是对应的jsperf网址:http://jsperf.com/splice-vs-push - Drew
1
+1 感谢您添加了这个内容和性能比较,省去了我测试这种方法的麻烦。 - jjrv
3
如果数组b很大,使用splice.apply或push.apply可能会因堆栈溢出而失败。它们也比使用“for”或“forEach”循环慢-请参见此jsPerf:http://jsperf.com/array-extending-push-vs-concat/5和我的答案https://dev59.com/VnM_5IYBdhLWcg3wcSx_#17368101。 - jcdude

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接