如何使用另一个数组扩展现有的JavaScript数组,而无需创建新数组

似乎没有办法用另一个数组扩展现有的JavaScript数组,即模拟Python的extend方法。

我想实现以下目标:

>>> a = [1, 2][1, 2]>>> b = [3, 4, 5][3, 4, 5]>>> SOMETHING HERE>>> a[1, 2, 3, 4, 5]

我知道有一个a.concat(b)方法,但它创建了一个新数组,而不是简单地扩展第一个数组。我想要一个在a明显大于b时有效工作的算法(即不复制a的算法)。

注意:这不是如何将某些内容附加到数组中?-这里的目标是将一个数组的全部内容添加到另一个数组中,并“就地”执行,即不复制扩展数组的所有元素。

892876 次浏览

#0方法可以接受多个参数。您可以使用点差算子将第二个数组的所有元素作为参数传递给.push

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

如果您的浏览器不支持ECMAScript 6,您可以使用#0代替:

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

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

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

请注意,如果数组b太长,所有这些解决方案都将失败并出现堆栈溢出错误(问题从大约100,000个元素开始,具体取决于浏览器)。
如果你不能保证b足够短,你应该使用另一个答案中描述的标准基于循环的技术。

使用splice()可以做到这一点:

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

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

我喜欢上面描述的a.push.apply(a, b)方法,如果你愿意,你可以像这样创建一个库函数:

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

并像这样使用它

a = [1,2]b = [3,4]
a.append(b)

您应该使用基于循环的技术。此页面上基于使用.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中的大条目大约>150,000个,Firefox中的条目>500,000个)。您可以在这个jperf中看到此错误。

当以大数组作为第二个参数调用'Function.prototype.apply'时,超出调用堆栈大小而发生错误。(MDN有关于使用Function.prototype.apply超过调用堆栈大小的危险的说明-请参阅标题为“应用和内置函数”的部分。)

有关此页面上其他答案的速度比较,请查看这个jperf(感谢EaterOfCode)。基于循环的实现在速度上与使用Array.push.apply相似,但往往比Array.slice.apply慢一点。

有趣的是,如果您附加的数组是稀疏的,上面的基于forEach的方法可以利用稀疏性并优于基于.apply的方法;如果您想亲自测试,请查看这个jperf

顺便说一句,不要被诱惑(就像我一样!)进一步缩短for每个实现为:

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

因为这会产生垃圾结果!为什么?因为Array.prototype.forEach为它调用的函数提供了三个参数-它们是:(element_value,element_index,source_array)。如果你使用“for每个(this.push, this)”,所有这些都会被推送到forEach的每次迭代的第一个数组上!

结合答案…

Array.prototype.extend = function(array) {if (array.length < 150000) {this.push.apply(this, array)} else {for (var i = 0, len = array.length; i < len; ++i) {this.push(array[i]);};}}

如果你想使用jQuery,有合并方法

示例:

a = [1, 2];b = [3, 4, 5];$.merge(a,b);

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

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];

(它也复制。

您可以像我下面所说的那样为扩展创建一个Poly填充。它将添加到数组中;就地并返回自身,以便您可以链接其他方法。

if (Array.prototype.extend === undefined) {Array.prototype.extend = function(other) {this.push.apply(this, arguments.length > 1 ? arguments : other);return this;};}
function print() {document.body.innerHTML += [].map.call(arguments, function(item) {return typeof item === 'object' ? JSON.stringify(item) : item;}).join(' ') + '\n';}document.body.innerHTML = '';
var a = [1, 2, 3];var b = [4, 5, 6];
print('Concat');print('(1)', a.concat(b));print('(2)', a.concat(b));print('(3)', a.concat(4, 5, 6));
print('\nExtend');print('(1)', a.extend(b));print('(2)', a.extend(b));print('(3)', a.extend(4, 5, 6));
body {font-family: monospace;white-space: pre;}

答案非常简单。

>>> a = [1, 2][1, 2]>>> b = [3, 4, 5][3, 4, 5]>>> SOMETHING HERE(The following code will combine the two arrays.)
a = a.concat(b);
>>> a[1, 2, 3, 4, 5]

Concat的行为与JavaScript字符串连接非常相似。它将返回您在调用函数的数组末尾放入conat函数的参数组合。关键是您必须将返回值分配给变量,否则它会丢失。例如

a.concat(b);  <--- This does absolutely nothing since it is just returning the combined arrays, but it doesn't do anything with it.

这些天我觉得最优雅的是:

arr1.push(...arr2);

关于点差运算符的MDN文章在ES2015(ES6)中提到了这种甜蜜的方式:

更好的推动

示例:推送通常用于将数组推送到现有数组的末尾数组。在ES5中,这通常是这样做的:

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

在ES6中,这变成了:

var arr1 = [0, 1, 2];var arr2 = [3, 4, 5];arr1.push(...arr2);

请注意,arr2不能太大(保持在大约100,000个项目以下),因为调用堆栈溢出,正如jc的答案所示。

合并两个以上数组的另一种解决方案

var a = [1, 2],b = [3, 4, 5],c = [6, 7];
// Merge the contents of multiple arrays together into the first arrayvar mergeArrays = function() {var i, len = arguments.length;if (len > 1) {for (i = 1; i < len; i++) {arguments[0].push.apply(arguments[0], arguments[i]);}}};

然后调用并打印为:

mergeArrays(a, b, c);console.log(a)

输出将是:Array [1, 2, 3, 4, 5, 6, 7]

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

apply()方法调用具有给定this值的函数,并且参数作为数组提供。

Push期望将列表项添加到数组中。然而,apply()方法将函数调用的预期参数作为数组。这允许我们使用内置push()方法轻松地将一个数组的元素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];

更短更好,但目前并非所有浏览器都完全支持。

此外,如果您想移动从数组ba的所有内容,在此过程中清空b,您可以这样做:

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

结果将如下:

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

这个解决方案适合我(使用ECMAScript 6的扩展运算符):

let array = ['my', 'solution', 'works'];let newArray = [];let newArray2 = [];newArray.push(...array); // Adding to same arraynewArray2.push([...array]); // Adding as child/leaf/sub-arrayconsole.log(newArray);console.log(newArray2);

对于>150,000条记录,使用Array.extend而不是Array.push

if (!Array.prototype.extend) {Array.prototype.extend = function(arr) {if (!Array.isArray(arr)) {return this;}
for (let record of arr) {this.push(record);}
return this;};}

超级简单,不指望传播运营商或应用,如果这是一个问题。

b.map(x => a.push(x));

在对此进行了一些性能测试后,它非常慢,但回答了关于不创建新数组的问题。Concat明显更快,即使是jQuery的$.merge()也让它感到遗憾。

https://jsperf.com/merge-arrays19b/1

您可以通过简单地在push()方法的帮助下向数组添加新元素来做到这一点。

let colors = ["Red", "Blue", "Orange"];console.log('Array before push: ' + colors);// append new value to the arraycolors.push("Green");console.log('Array after push : ' + colors);

另一种用于将元素附加到数组开头的方法是unShift()函数,它添加并返回新的长度。它接受多个参数,附加现有元素的索引,最后返回数组的新长度:

let colors = ["Red", "Blue", "Orange"];console.log('Array before unshift: ' + colors);// append new value to the arraycolors.unshift("Black", "Green");console.log('Array after unshift : ' + colors);

还有其他方法。您可以检查它们这里

概览

  • 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留档:扩展语法.应用. concat(). ush()

速度考虑

如果ab都很小,那么每个方法都很快,因此在大多数Web应用程序中,您需要使用push(...b)并完成它。

如果你要处理超过几千个元素,你想做什么取决于具体情况:

  • 您正在向一个大数组添加一些元素
    push(...b)非常快
  • 您正在向一个大数组添加许多元素
    concat比循环略快
  • 您正在向一个小数组添加许多元素
    concat比循环快得多
  • 你是通常只向任何大小的数组添加几个元素
    →循环与小添加的有限方法一样快,但永远不会抛出异常,即使它在添加许多元素时不是最高性能的
  • 您正在编写一个包装函数以始终获得最大性能
    →您需要动态检查输入的长度并选择正确的方法,也许在循环中调用push(...b_part)(使用大b的切片)。

这让我感到惊讶:我认为a=a.concat(b)能够在a上做一个很好的memcpy,而不必像a.push(...b)那样费心做单独的扩展操作,因此总是最快的。相反,a.push(...b)要快得多,特别是当a很大的时候。

不同方法的速度在Firefox 88Linux使用以下方法测量:

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 testconsole.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])

请注意,50万的Bsize是我的系统上所有方法接受的最大值,这就是为什么它小于Asize

所有测试都运行了多次,以查看结果是否异常值或代表性。当然,快速方法在使用performance.now()的一次运行中几乎无法测量,但是由于慢方法是如此明显,并且两种快速方法都在相同的原理上工作,我们不必费心重复一堆次来分裂头发。

如果任一数组都很大,concat方法总是很慢,但只有当它必须执行大量函数调用并且不在乎a有多大时,循环才会很慢。因此,对于小b,循环类似于push(...b)push.apply,但如果它变大,则不会中断;然而,当你接近极限时,concat又快了一点。

另一种选择,如果你已经安装了Ladash:

 import { merge } from 'lodash';
var arr1 = merge(arr1, arr2);

我添加这个答案,因为尽管问题清楚地说明了without creating a new array,几乎每个答案都忽略了它。

现代JavaScript与数组以及类似的可迭代对象一起工作得很好。这使得实现构建在其上的concat版本成为可能,并在逻辑上将数组数据跨越其参数。

下面的示例使用了具有此类逻辑的it-ops库:

import {pipe, concat} from 'iter-ops';
const i = pipe(originalArray,concat(array2, array3, array4, ...)); //=> Iterable
for(const a of i) {console.log(a); // iterate over values from all arrays}

上面,没有创建新数组。操作符conat将遍历原始数组,然后将自动按指定顺序继续进入array2,然后array3,依此类推。

就内存使用而言,这是连接数组的最有效方式。

如果最后决定将其转换为实际的物理数组,则可以通过扩展运算符或Array.from进行操作:

const fullArray1 = [...i]; // pulls all values from iterable, into a new array
const fullArray2 = Array.from(i); // does the same

正如投票最多的答案所说,考虑到尺寸限制问题,a.push(...b)可能是正确的答案。

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

以下数据是2022-05-20的数据

在此处输入图片描述

在此处输入图片描述

在此处输入图片描述

这里

目前看来,push在2022年是最快的。这在未来可能会改变。

忽略问题(生成新数组)的答案没有抓住重点。鉴于可能存在对同一数组的其他引用,许多代码可能需要/想要修改一个数组

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的答案可以说是糟糕的。更改原生原型基本上是你代码中的地雷。另一个实现可能与你的不同,所以它会破坏你的代码,或者你会在期待其他行为的情况下破坏他们的代码。这包括是否/何时添加与你的冲突的原生实现。你可能会说“我知道我在用什么,所以没有问题”,这可能是真的,你现在是一个开发人员,但添加第二个开发人员,他们无法读懂你的想法。而且,你是几年后的第二个开发人员,当你忘记了,然后将其他一些库(分析?,日志记录?,…)移植到你的页面上,忘记了你在代码中留下的想法。

这不仅仅是理论。网上有无数关于人们撞上地雷的故事。

可以说,修改原生对象的原型只有几种安全用途。一种是在旧浏览器中多边形填充现有和指定的实现。在这种情况下,规范被定义,规范被实现在新浏览器中发布,你只是想在旧浏览器中获得相同的行为。这是非常安全的。预补丁(规范正在进行中,但没有发布)可以说是不安全的。规格在发布前会改变。