将数组项复制到另一个数组中。

1214

我有一个JavaScript数组dataArray,想将其推入一个新数组newArray。但是我不想让newArray[0]成为dataArray。我希望将所有项都推入新数组中:

var newArray = [];

newArray.pushValues(dataArray1);
newArray.pushValues(dataArray2);
// ...

或者更好的是:

var newArray = new Array (
   dataArray1.values(),
   dataArray2.values(),
   // ... where values() (or something equivalent) would push the individual values into the array, rather than the array itself
);

现在,新数组包含了所有个别数据数组的值。是否有一些类似于pushValues的速记方法可用,以便我不必逐个迭代每个dataArray,逐一添加项目?


请参阅此网址https://dev59.com/JXRC5IYBdhLWcg3wSu57 - Jakir Hossain
这应该是答案 https://davidwalsh.name/combining-js-arrays - starikovs
这个回答是否解决了你的问题?如何在JavaScript中合并两个数组并去重 - HackerMan
20个回答

1582

使用concat函数,如下所示:

var arrayA = [1, 2];
var arrayB = [3, 4];
var newArray = arrayA.concat(arrayB);

newArray 的值将会是 [1, 2, 3, 4]arrayAarrayB 不变;concat 创建并返回一个包含结果的新数组)。


19
我同意,执行效率很好。但是 concat 不就是为了将数组 拼接 起来的吗?所以它应该是 标准 的。或者说 concat 还有更好的用途吗?而且它可能只是因为浏览器或其他地方的 JS 引擎实现不佳而变慢。这个问题可能会在未来得到解决。我会选择代码可维护性而不是仅仅追求速度优化。嗯... - Bitterblue
7
我刚刚对情况进行了基准测试:concat与push.apply的比较。在谷歌浏览器中,concat更快(成为赢家),在欧朋浏览器中,concat也更快(成为赢家),但在IE浏览器中,concat较慢(依然是赢家)。而在火狐浏览器中,则更慢(push.apply成为赢家,但比谷歌浏览器的concat慢10倍)……这可真是JS引擎实现不佳的例子。 - Bitterblue
52
如何将两个数组连接成一个数组是如何将一个数组推入另一个数组的可接受答案?这是两个不同的操作。 - kaqqao
4
因为push不能展开一个值的数组,所以需要使用concat来实现问题所需的功能。 - WiseGuyEh
5
想象一下这个函数:function(stuff, toAdd) {stuff.push(toAdd);} 这里无法使用concat。这就是我所说的它们不能互换的原因。这也是我相信Array.prototype.push.apply(stuff, toAdd);是正确答案的原因。 - kaqqao
显示剩余5条评论

829
在ECMAScript 6中,您可以使用展开语法

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

console.log(arr1)

展开语法在所有主要浏览器中都可用(不包括IE11)。有关当前兼容性,请参见此(持续更新的)兼容性表

但是,请参见杰克·吉芬下面的回复,以获取有关性能的更多评论。看起来concat仍然比展开运算符更好、更快。


11
如果你正在使用TypeScript,你也可以使用扩展运算符。如果你的目标是ES5,它会被编译成newArray.apply(newArray, dataArray1) - AJ Richardson
11
请注意:如果您需要将结果存储在第三个数组中(因此不会修改arr1,就像最初的问题所要求的那样),您可以执行newArray = [...arr1, ...arr2]。 - dim
顺便提一下,与其他将一个数组的内容复制到另一个数组的方法相比,这也是极快的。 - Engineer
如果您使用扩展运算符传递了许多元素,您将遇到“最大调用堆栈错误”,因为您基本上将调用一个具有巨大数量参数的函数。 - Steven Spungin
这应该是被接受的答案。 - undefined
显示剩余2条评论

684

如果你的数组不是很大(见下面的注意事项),你可以使用想要添加值的数组的push()方法。 push()可以接受多个参数,因此您可以使用其apply()方法将要推送的值的数组作为函数参数列表传递。与使用concat()相比,这种方法的优点在于直接对数组进行添加元素,而不是创建一个新数组。

然而,似乎对于大型数组(约100,000个或更多成员的数组),这种技巧可能会失败。 对于这样的数组,使用循环是更好的方法。有关详细信息,请参见https://dev59.com/VnM_5IYBdhLWcg3wcSx_#17368101

var newArray = [];
newArray.push.apply(newArray, dataArray1);
newArray.push.apply(newArray, dataArray2);

你可能希望将其泛化为一个函数:

function pushArray(arr, arr2) {
    arr.push.apply(arr, arr2);
}

... 或将其添加到Array原型中:

Array.prototype.pushArray = function(arr) {
    this.push.apply(this, arr);
};

var newArray = [];
newArray.pushArray(dataArray1);
newArray.pushArray(dataArray2);

或者通过利用concat()允许多个参数的特性来模拟原始的push()方法:

Array.prototype.pushArray = function() {
    this.push.apply(this, this.concat.apply([], arguments));
};

var newArray = [];
newArray.pushArray(dataArray1, dataArray2);

以下是最后一个示例的基于循环的版本,适用于大型数组和包括 IE <= 8 在内的所有主要浏览器:

Array.prototype.pushArray = function() {
    var toPush = this.concat.apply([], arguments);
    for (var i = 0, len = toPush.length; i < len; ++i) {
        this.push(toPush[i]);
    }
};

10
注意:newArray.push.apply(newArray, dataArray1);Array.prototype.push.applay(newArray,dataArra1); 是相同的。 - user669677
这个兼容老旧的浏览器吗? - Damien
1
@DamienÓCeallaigh:是的。这一切都兼容IE 6甚至更早期的浏览器。 - Tim Down
这是糟糕的编程实践的典范。Array.prototype.concat并不是坏的,而只是被误解和有时被滥用。在这些情况下,它只是被误解了,但并没有被滥用。 - Jack G
7
使用扩展运算符,可以将其简化为 array.push(...array2); - Moshe Sommers
显示剩余4条评论

164

MDN 中找到了一种优雅的方法


var vegetables = ['parsnip', 'potato'];
var moreVegs = ['celery', 'beetroot'];

// Merge the second array into the first one
// Equivalent to vegetables.push('celery', 'beetroot');
Array.prototype.push.apply(vegetables, moreVegs);

console.log(vegetables); // ['parsnip', 'potato', 'celery', 'beetroot']

或者你可以使用ES6的展开运算符特性:

let fruits = [ 'apple', 'banana'];
const moreFruits = [ 'orange', 'plum' ];

fruits.push(...moreFruits); // ["apple", "banana", "orange", "plum"]

2
这不是问题所要求的。问题要求每次创建一个新数组,而不是修改旧数组。 - Jack G

22

(关于原始问题)

为了获得事实,对jsperf上的性能测试和控制台中的一些内容进行了检查。在研究方面,使用了irt.org网站。下面是所有这些来源汇总在一起,并附有一个示例函数。

╔═══════════════╦══════╦══════════════════╦══════════════╦═════════╦════════╗
║ 方法           ║拼接   ║slice&push.apply║ push.apply x2║ For循环  ║展开     ║
╠═══════════════╬══════╬══════════════════╬══════════════╬═════════╬════════╣
║ mOps/Sec      ║179   ║104               ║ 76           ║ 81      ║28       ║
╠═══════════════╬══════╬══════════════════╬══════════════╬═════════╬════════╣
║ 稀疏数组       ║是    ║仅保留切片          ║ 否            ║ 或许2  ║否        ║
║ 保持稀疏      ║      ║数组(第一个参数)      ║              ║         ║         ║
╠═══════════════╬══════╬══════════════════╬══════════════╬═════════╬════════╣
║ 支持           ║MSIE 4║MSIE 5.5          ║ MSIE 5.5      ║ MSIE 4  ║Edge 12  ║
║ (source)      ║NNav 4║NNav 4.06        ║ NNav 4.06     ║ NNav 3  ║MSIE NNav ║
╠═══════════════╬══════╬══════════════════╬══════════════╬═════════╬════════╣
║类似数组的行为   ║否    ║仅保留推送          ║ 是            ║ 是       ║如果有迭代器则是1  ║
║像一个数组     ║      ║数组(第二个参数)      ║              ║         ║         ║
╚═══════════════╩══════╩══════════════════╩══════════════╩═════════╩════════╝
1 如果类似数组对象没有Symbol.iterator属性,则尝试展开它将抛出异常。
2 取决于代码。下面的示例代码“是”保留了稀疏性。
function mergeCopyTogether(inputOne, inputTwo){
   var oneLen = inputOne.length, twoLen = inputTwo.length;
   var newArr = [], newLen = newArr.length = oneLen + twoLen;
   for (var i=0, tmp=inputOne[0]; i !== oneLen; ++i) {
        tmp = inputOne[i];
        if (tmp !== undefined || inputOne.hasOwnProperty(i)) newArr[i] = tmp;
    }
    for (var two=0; i !== newLen; ++i, ++two) {
        tmp = inputTwo[two];
        if (tmp !== undefined || inputTwo.hasOwnProperty(two)) newArr[i] = tmp;
    }
    return newArr;
}

如上所示,我认为Concat几乎总是性能最佳的选择,并且具有保留稀疏数组特性的能力。对于类似数组的对象(例如像document.body.children这样的DOMNodeList),我建议使用for循环,因为它既是第二快的方法,也是唯一保留稀疏数组的另一种方法。下面我们将简要介绍什么是稀疏数组和类似数组,以消除困惑。

起初,有些人可能会认为这只是一种偶然情况,浏览器供应商最终会优化Array.prototype.push以使其足够快,以打败Array.prototype.concat。错误! Array.prototype.concat总是更快的(至少在原则上),因为它只是在数据上进行了简单的复制粘贴。下面是一个简化的可视化图表,展示了32位数组实现的外观(请注意,实际实现要复杂得多)

Byte ║ 数据在此
═════╬═══════════
0x00 ║ int nonNumericPropertiesLength = 0x00000000
0x01 ║ 同上
0x02 ║ 同上
0x03 ║ 同上
0x00 ║ int length = 0x00000001
0x01 ║ 同上
0x02 ║ 同上
0x03 ║ 同上
0x00 ║ int valueIndex = 0x00000000
0x01 ║ 同上
0x02 ║ 同上
0x03 ║ 同上
0x00 ║ int valueType = JS_PRIMITIVE_NUMBER
0x01 ║ 同上
0x02 ║ 同上
0x03 ║ 同上
0x00 ║ uintptr_t valuePointer = 0x38d9eb60(或内存中的任何位置)
0x01 ║ 同上
0x02 ║ 同上
0x03 ║ 同上

如上所示,你需要做的就是将其复制,几乎就像逐字节复制那样简单。而对于Array.prototype.push.apply,则远不止在数据上进行了简单的复制粘贴。".apply"必须检查数组中的每个索引并将其转换为一组参数,然后将其传递给Array.prototype.push。然后,Array.prototype.push还必须每次分配更多的内存,并且(对于某些浏览器实现)甚至可能重新计算稀疏度的位置查找数据。

  1. 去商店买足够的纸张,以复制每个源数组所需的数量。然后将每个源数组的纸张堆放在复印机上,将生成的两份副本装订在一起。
  2. 去商店买足够第一个源数组的单份拷贝所需的纸张。然后手动将源数组复制到新纸张上,确保填补任何空白稀疏点。然后再回到商店,购买第二个源数组所需的纸张。接着,遍历第二个源数组并复制它,同时确保复制品中没有空白间隙。最后将所有复制好的纸张装订在一起。

以上类比中,选项#1代表Array.prototype.concat,而#2代表Array.prototype.push.apply。让我们使用一个类似的JSperf测试来检验它,唯一的区别在于这个测试针对的是稀疏数组而不是实数组。您可以在这里找到它:这里

因此,我论断这种特定用例的性能未来不在于Array.prototype.push,而在于Array.prototype.concat。

当数组的某些成员被简单地省略时。例如:

// This is just as an example. In actual code, 
// do not mix different types like this.
var mySparseArray = [];
mySparseArray[0] = "foo";
mySparseArray[10] = undefined;
mySparseArray[11] = {};
mySparseArray[12] =  10;
mySparseArray[17] = "bar";
console.log("Length:   ", mySparseArray.length);
console.log("0 in it:  ", 0 in mySparseArray);
console.log("arr[0]:   ", mySparseArray[0]);
console.log("10 in it: ", 10 in mySparseArray);
console.log("arr[10]   ", mySparseArray[10]);
console.log("20 in it: ", 20 in mySparseArray);
console.log("arr[20]:  ", mySparseArray[20]);

另外,JavaScript使得您可以轻松地初始化稀疏数组。

var mySparseArray = ["foo",,,,,,,,,,undefined,{},10,,,,,"bar"];

-

类数组(array-like)指的是至少具有 length 属性,但未使用 new Array[] 初始化的对象;例如以下对象都被归类为类数组。

{0: "foo", 1: "bar", length:2}
document.body.children
new Uint8Array(3)
  • 这是一种类数组,因为虽然它是一个(类型化的)数组,但将其强制转换为数组会更改构造函数。
(function(){return arguments})()

使用像 slice 这样将类数组强制转换为数组的方法时,请注意发生的情况。

var slice = Array.prototype.slice;
// For arrays:
console.log(slice.call(["not an array-like, rather a real array"]));
// For array-likes:
console.log(slice.call({0: "foo", 1: "bar", length:2}));
console.log(slice.call(document.body.children));
console.log(slice.call(new Uint8Array(3)));
console.log(slice.call( function(){return arguments}() ));

  • 注意:由于性能原因,在函数参数上调用slice是不好的实践。

观察使用一个不会像concat那样强制将类数组转换为数组的方法会发生什么。

var empty = [];
// For arrays:
console.log(empty.concat(["not an array-like, rather a real array"]));
// For array-likes:
console.log(empty.concat({0: "foo", 1: "bar", length:2}));
console.log(empty.concat(document.body.children));
console.log(empty.concat(new Uint8Array(3)));
console.log(empty.concat( function(){return arguments}() ));


21

对我来说,以下看起来最简单:

var newArray = dataArray1.slice();
newArray.push.apply(newArray, dataArray2);

"push"方法可以接受可变数量的参数,你可以使用push函数的apply方法将另一个数组的所有元素推入它。它会使用它的第一个参数("newArray"在这里)作为"this",并将数组的元素作为剩余的参数来构建调用push的方式。

第一条语句中的slice获取了第一个数组的副本,因此不会修改它。

更新如果你的javascript版本支持slice,你可以将push表达式简化为:

newArray.push(...dataArray2)

1
在MDN上也提到了它作为“合并两个数组”的示例,链接在此:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/push - Wilt

19

使用JavaScript ES6,您可以使用展开运算符作为展开运算符,它将基本上将数组转换为值。然后,您可以执行以下操作:

const myArray = [1,2,3,4,5];
const moreData = [6,7,8,9,10];

const newArray = [
  ...myArray,
  ...moreData,
];

虽然语法简洁,但我不知道它在内部是如何工作的,以及在处理大型数组时的性能影响。


2
如果您查看 babel 如何将其转换,您会发现它不应该比使用 Array.push.apply 技术更慢。 - emil.c
1
@JackGiffin,我只是在参考Ryan提到的他不知道内部工作原理和性能影响是什么,我并没有真正建议采用这种方法。无论如何,你在回答中做得非常好,研究得很好,了解这些细节总是很好的。 - emil.c

16
var a=new Array('a','b','c');
var b=new Array('d','e','f');
var d=new Array('x','y','z');
var c=a.concat(b,d)

那解决了你的问题吗?


13
我将提供两种简单的方法来完成这个操作: 解决方案1:
let dataArray1= [0, 1];
let dataArray1= [2, 3];
dataArray1.push(...dataArray2);
console.log(dataArray1) 

解决方案2:

let dataArray1 = [1, 2];
let dataArray2 = [3, 4];
let newArray = dataArray1.concat(dataArray2);
console.log(newArray)

13

以下函数没有与数组长度相关的问题,并且性能优于所有建议的解决方案:

function pushArray(list, other) {
    var len = other.length;
    var start = list.length;
    list.length = start + len;
    for (var i = 0; i < len; i++ , start++) {
        list[start] = other[i];
    }
}

不幸的是,jspref拒绝接受我的提交,所以这里使用benchmark.js得出了结果。

        Name            |   ops/sec   |  ± %  | runs sampled
for loop and push       |      177506 |  0.92 | 63
Push Apply              |      234280 |  0.77 | 66
spread operator         |      259725 |  0.40 | 67
set length and for loop |      284223 |  0.41 | 66

其中

for循环和push操作是:

    for (var i = 0, l = source.length; i < l; i++) {
        target.push(source[i]);
    }

点击“应用”:

target.push.apply(target, source);

展开运算符:

    target.push(...source);

最后,“设置长度和循环”是上述函数的一部分。


这个问题是在寻找一种每次创建新数组的方法,而不是修改现有数组。 - Jack G

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