【问题标题】:How to extend an existing JavaScript array with another array, without creating a new array如何在不创建新数组的情况下使用另一个数组扩展现有的 JavaScript 数组
【发布时间】:2023-02-11 14:01:13
【问题描述】:

似乎没有办法用另一个数组扩展现有的 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)时有效工作的算法。

笔记:这是不是How to append something to an array?的副本-- 这里的目标是将一个数组的全部内容添加到另一个数组,并“就地”进行,即不复制扩展数组的所有元素。

【问题讨论】:

  • 来自@Toothbrush 对答案的评论:a.push(...b)。它在概念上与最佳答案相似,但针对 ES6 进行了更新。
  • >>> a.push(...b)

标签: javascript arrays concatenation


【解决方案1】:

.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 足够短,您应该使用其他答案中描述的基于循环的标准技术。

【讨论】:

  • 我认为这是你最好的选择。其他任何事情都将涉及迭代或 apply() 的另一项努力
  • 如果“b”(要扩展的数组)很大(根据我的测试,Chrome 中大约有 > 150000 个条目),此答案将失败。您应该使用 for 循环,或者更好地使用“b”上的内置“forEach”函数。看我的回答:stackoverflow.com/questions/1374126/…
  • @Deqing:Array 的push 方法可以接受任意数量的参数,然后将这些参数推到数组的后面。所以 a.push('x', 'y', 'z') 是一个有效的调用,它将 a 扩展 3 个元素。 apply 是任何函数的一种方法,它接受一个数组并使用它的元素,就好像它们都被明确地作为函数的位置元素一样。所以 a.push.apply(a, ['x', 'y', 'z']) 也会将数组扩展 3 个元素。此外,apply 将上下文作为第一个参数(我们再次传递 a 以附加到 a)。
  • 注意:在这种情况下,第一个返回值不是 [1,2,3,4,5] 而是 5 而 a == [1,2,3,4,5] 之后。
  • 另一个稍微不那么令人困惑(而且不会更长)的调用是:[].push.apply(a, b)
【解决方案2】:

2018 年更新A better answer is a newer one of minea.push(...b)。不要再给这个投票了,因为它从来没有真正回答过这个问题,但它是 2015 年围绕谷歌首次点击的黑客攻击:)


对于那些只是搜索“JavaScript array extend”并到达这里的人,你可以很好地使用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];

(它也复制。)

【讨论】:

  • 问题很明确:“不创建新阵列?”
  • @Wilt:这是真的,答案说明了原因:)这是很长一段时间以来首次出现在谷歌上。其他解决方案也很丑陋,想要一些理想且漂亮的东西。虽然现在 arr.push(...arr2) 更新更好,并且是对这个特定问题的技术正确(tm)答案。
  • 我听到了,但我在寻找“javascript 追加数组而不创建新数组”,然后很遗憾地看到一个 60 次高票的答案并没有回答实际问题;)我没有投反对票,因为你在你的回答中说得很清楚。
  • @Wilt 和我搜索了“js extend array with array”并找到了这里,谢谢 :)
【解决方案3】:

您应该使用基于循环的技术。此页面上基于使用 .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's answer(使用 push.apply)和其他基于.apply 的方法会失败(测试表明,对我来说,在 Chrome 中大的条目大约是 > 150,000 个条目,在 Firefox 中是 > 500,000 个条目)。你可以看到这个错误发生在this jsperf

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

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

有趣的是,如果你追加的数组是稀疏的,上面基于forEach的方法可以利用稀疏性并优于基于.apply的方法;如果您想自己测试一下,请查看this jsperf

顺便说一下,不要(像我一样!)进一步缩短 forEach 实现:

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

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

【讨论】:

  • 附:要测试 other_array 是否真的是一个数组,请选择此处描述的选项之一:stackoverflow.com/questions/767486/…
  • 好答案。我不知道这个问题。我在这里引用了你的答案:stackoverflow.com/a/4156156/96100
  • .push.apply 实际上比 v8 中的 .forEach 快很多,最快的仍然是内联循环。
  • @BenjaminGruenbaum - 你能发布一些结果的链接吗?据我从本评论末尾链接的 jsperf 收集的结果可以看出,在 Chrome/Chromium(自 v25 以来的所有版本)中使用 .forEach.push.apply 更快。我无法单独测试 v8,但如果你有,请链接你的结果。参见jsperf:jsperf.com/array-extending-push-vs-concat/5
  • @jcdude 你的 jsperf 无效。因为数组中的所有元素都是undefined,所以.forEach 将跳过它们,使其成为最快的。 .splice居然是最快的?! jsperf.com/array-extending-push-vs-concat/18
【解决方案4】:

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

arr1.push(...arr2);

MDN article on the spread operator 在 ES2015 (ES6) 中提到了这种美妙的含糖方式:

更好的推动

示例:push 通常用于将数组推到现有数组的末尾 大批。在 ES5 中,这通常是这样完成的:

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

在带有 spread 的 ES6 中,这变成了:

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

请注意,arr2 不能很大(保持在大约 100 000 个项目以下),因为根据 jcdude 的回答,调用堆栈会溢出。

【讨论】:

  • 只是对我刚刚犯的错误的警告。 ES6版本非常接近arr1.push(arr2)。这可能是一个像 arr1 = []; arr2=['a', 'b', 'd']; arr1.push(arr2) 这样的问题,结果是一个数组数组 arr1 == [['a','b','d']] 而不是两个数组的组合。这是一个容易犯的错误。出于这个原因,我更喜欢下面的第二个答案。 stackoverflow.com/a/31521404/4808079
  • 使用.concat 的一个便利之处在于第二个参数不能是数组。这可以通过将扩展运算符与 concat 组合来实现,例如arr1.push(...[].concat(arrayOrSingleItem))
  • 对于目标数组不为空的情况,这是现代的、优雅的 Javascript。
  • 这够快吗?
【解决方案5】:

概述

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

每个 sn-p 修改 a 以扩展 b

“有限的”sn-ps 将每个数组元素作为参数传递,并且the maximum number of arguments you can pass to a function is limited。从那个链接来看,a.push(...b) 似乎是可靠的,直到 b 中有大约 32k 个元素(a 的大小无关紧要)。

相关 MDN 文档:spread syntax.apply().concat().push()

速度考虑

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

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

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

这让我感到惊讶:我认为 a=a.concat(b) 可以将 b 做一个很好的 memcpy 到 a 而不必像 a.push(...b) 那样做单独的扩展操作,因此总是最快的。相反,a.push(...b) 快得多,尤其是当 a 很大时。

不同方法的速度是在 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 的 Bsize 是我系统上所有方法接受的最大值,这就是它小于 Asize 的原因。

所有测试都运行了多次,以查看结果是否异常或具有代表性。当然,使用 performance.now() 运行一次,快速方法几乎是不可估量的,但是由于慢速方法非常明显,而且两种快速方法都基于相同的原理,所以我们不必费心重复它很多次分裂的头发。

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

【讨论】:

    【解决方案6】:

    首先说说 JavaScript 中的 apply() 以帮助理解我们为什么使用它:

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

    推送期望一个列表要添加到数组的项目。但是,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 = [];
    

    【讨论】:

    • 或者while (b.length) { a.push(b.shift()); },对吧?
    【解决方案7】:

    如果你想用jQuery,有$.merge()

    例子:

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

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

    【讨论】:

    【解决方案8】:

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

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

    并像这样使用它

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

    【讨论】:

    • 不应使用 push.apply 方法,因为如果您的“array”参数是一个大数组(例如 Chrome 中 > ~150000 个条目),它可能会导致堆栈溢出(因此失败)。你应该使用“array.forEach”——看我的回答:stackoverflow.com/a/17368101/1280629
    • 在 ES6 中,只需使用 a.push(...b)
    【解决方案9】:

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

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

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

    【讨论】:

    【解决方案10】:

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

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

    以下这些数字适用于 2022-05-20

    来自here

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

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

    可以说,修改本机对象的原型只有一些安全用途。一种是在旧浏览器中填充现有的和指定的实现。在这种情况下,规范已定义,规范已在新浏览器中实现,您只想在旧浏览器中获得相同的行为。那很安全。预修补(规范正在进行中但尚未发布)可以说是不安全的。规格在发货前更改。

    【讨论】:

      【解决方案11】:

      此解决方案适用于我(使用 ECMAScript 6 的扩展运算符):

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

      【讨论】:

        【解决方案12】:

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

        现代 JavaScript 可以很好地处理数组和类似的可迭代对象。这使得实现基于此构建的 concat 版本成为可能,并在逻辑上跨越其参数的数组数据。

        下面的示例使用了具有此类逻辑的 iter-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
        }
        

        上面,没有创建新数组。运算符concat 将遍历原始数组,然后自动继续进入array2,然后是array3,依此类推,以指定的顺序进行。

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

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

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

        【讨论】:

          【解决方案13】:

          结合答案...

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

          【讨论】:

          • 不,没有必要这样做。使用 forEach 的 for 循环比使用 push.apply 更快,并且无论要扩展的数组的长度是多少,它都可以工作。看看我修改后的答案:stackoverflow.com/a/17368101/1280629 无论如何,你怎么知道 150000 是适用于所有浏览器的正确数字?这是软糖。
          • 哈哈。我的答案无法辨认,但似乎是当时发现的其他人的一些组合 - 即摘要。不用担心
          【解决方案14】:

          您可以为扩展创建一个 polyfill,如下所示。它将添加到数组中;就地并返回自身,以便您可以链接其他方法。

          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(' ') + '
          ';
          }
          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('
          Extend');
          print('(1)', a.extend(b));
          print('(2)', a.extend(b));
          print('(3)', a.extend(4, 5, 6));
          body {
            font-family: monospace;
            white-space: pre;
          }

          【讨论】:

            【解决方案15】:

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

            var a = [1, 2],
                b = [3, 4, 5],
                c = [6, 7];
            
            // Merge the contents of multiple arrays together into the first array
            var 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]

            【讨论】:

              【解决方案16】:

              答案非常简单。

              >>> 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 字符串连接非常相似。它将返回您在调用该函数的数组末尾放入 concat 函数的参数的组合。关键是您必须将返回值分配给变量,否则它会丢失。例如

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

              【讨论】:

              【解决方案17】:

              另一种选择,如果你安装了 lodash:

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

              【讨论】:

              • 我认为这不适用于这样的数组。请提供工作 sn-p 的链接。
              【解决方案18】:

              对于超过 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;
                };
              }
              

              【讨论】:

                【解决方案19】:

                您可以通过使用 push() 方法简单地向数组添加新元素来实现。

                let colors = ["Red", "Blue", "Orange"];
                console.log('Array before push: ' + colors);
                // append new value to the array
                colors.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 array
                colors.unshift("Black", "Green");
                console.log('Array after unshift : ' + colors);

                还有其他方法。你可以查看他们here

                【讨论】:

                • 这会向数组添加一个值。它不会根据问题将数组添加到数组(就地),也不会像 Python 的列表extend 方法那样。
                【解决方案20】:

                超级简单,不依赖传播运营商或申请,如果这是一个问题。

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

                在对此运行一些性能测试后,它非常慢,但回答了关于不创建新数组的问题。 Concat 明显更快,甚至 jQuery 的 $.merge() 也比它快。

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

                【讨论】:

                • 如果您不使用返回值,则应使用 .forEach 而不是 .map。它更好地传达了您的代码的意图。
                • @david - 每个人都有。我更喜欢.map 的语法,但你也可以使用b.forEach(function(x) { a.push(x)} )。事实上,我将其添加到 jsperf 测试中,它比 map 快一点点。还是超级慢。
                • 这不是偏好的情况。这些“功能性”方法各自具有与之相关的特定含义。在这里调用.map看起来很奇怪。这就像打电话给b.filter(x =&gt; a.push(x))。它有效,但它通过暗示某些事情正在发生而实际上并没有发生,从而使读者感到困惑。
                • @elPastor 这应该值得你花时间,因为像我这样的其他人会坐下来抓挠他的后颈,想知道你的代码中还发生了什么他看不到的事情。在大多数普通情况下,重要的是意图的清晰度,而不是表现。请尊重那些阅读你代码的人的时间,因为当你只是一个人时,他们可能很多。谢谢。
                • @elPastor 我确切地知道.map() 做了什么,这就是问题所在。这些知识会让我认为您需要结果数组,否则最好使用 .forEach() 让读者对回调函数的副作用保持谨慎。严格来说,您的解决方案创建了一个额外的自然数数组(push 的结果),而问题是:“不创建新数组”。
                猜你喜欢
                • 2010-11-25
                • 2019-12-27
                • 1970-01-01
                • 1970-01-01
                • 2015-12-30
                • 2022-01-22
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多