使用扩展语法(...)和push.apply在处理数组时的区别

38

我有两个数组,

const pets = ["dog", "cat", "hamster"]

const wishlist = ["bird", "snake"]

我想将wishlist添加到pets中,有两种方法可以实现:

方法1:

pets.push.apply(pets,wishlist)

转换后的结果为:[ 'dog', 'cat', 'hamster', 'bird', 'snake' ]

方法2:

pets.push(...wishlist)

这也导致了:[ 'dog', 'cat', 'hamster', 'bird', 'snake' ]

当我处理更大的数据时,这两种方法在性能方面有区别吗?


1
定义“更大”的数据 - Firefox使用方法2看起来比较慢...其他浏览器可能会更快...尝试自己进行基准测试。 - Jaromanda X
1
如果你将代码转译成ES5,很有可能Babel/TypeScript等工具会生成类似pets.push.apply的代码。无论如何,影响应用速度的性能差异几乎微乎其微。你为什么会有这个疑问呢? - user663031
1
在JS中,您可以向函数发送无限数量的参数,但不是无限制的。根据当前会话可用的堆栈大小,存在一个限制,例如最多150-300K个参数。根据您的问题,在我的基准测试中,push.apply似乎工作得更快。 - Redu
如果您不需要保留对数组的引用,则使用.concat()更为合适。 - a better oliver
您正在询问一个实现细节,这取决于浏览器和时间的响应(因为实现细节可能会改变)。从概念上讲,这两种技术是相同的。 - user6445533
7个回答

34

Function.prototype.apply 和扩展运算符(spread syntax)在应用于大数组时都可能导致堆栈溢出:

let xs = new Array(500000),
 ys = [], zs;

xs.fill("foo");

try {
  ys.push.apply(ys, xs);
} catch (e) {
  console.log("apply:", e.message)
}

try {
  ys.push(...xs);
} catch (e) {
  console.log("spread:", e.message)
}

zs = ys.concat(xs);
console.log("concat:", zs.length)

使用Array.prototype.concat替代。除了避免堆栈溢出之外,concat还具有避免突变的优点。 突变被认为是有害的,因为它们可能导致微妙的副作用。

但这并不是一种教条。 如果您在函数范围内执行突变以提高性能并减轻垃圾收集,则可以执行突变,只要它们不在父范围中可见即可。


5
使用 push 方法会在现有数组中添加元素,而使用扩展运算符则是创建一个新的数组副本。
a=[1,2,3]
b=a
a=[...a, 4]
alert(b);

=> 1, 2, 3

 a=[1,2,3]
b=a
a.push(4)
alert(b);

=> 1, 2, 3, 4

还有push.apply:

a=[1,2,3]
c=[4]
b=a
Array.prototype.push.apply(a,c)
alert(b);

=> 1, 2, 3, 4

concat是一种复制方法

a=[1,2,3]
c=[4]
b=a
a=a.concat(c)
alert(b);

=> 1、2、3

引用传递方式更适用于大型数组。

展开运算符是一种快速复制的方式,传统方式需要使用类似以下代码:

a=[1,2,3]
b=[]
a.forEach(i=>b.push(i))
a.push(4)
alert(b);

=> 1, 2, 3

如果你需要一份副本,请使用展开运算符,这样速度会更快。或者像@ftor指出的那样使用concat。如果不需要副本,则可以使用push。请记住,在某些情况下您无法进行变异。此外,使用任何这些函数都将得到浅层副本,而不是深层副本。如果需要深层副本,则需要使用lodash。在此处阅读更多信息:https://slemgrim.com/mutate-or-not-to-mutate/


4
对于一个大数组的附加操作,使用“展开运算符”比使用“concat()函数”快得多。“我不知道@ftor / @Liau Jian Jie是如何得出结论的,可能是因为测试不正确。”

test results Chrome 71.0.3578.80 (Official Build) (64-bit), FF 63.0.3 (64-bit), & Edge 42.17134.1.0

这是很有道理的,因为“concat()”创建了一个数组副本,而且并没有尝试使用相同的内存。 关于“变异”的事情似乎没有依据;如果您正在覆盖旧数组,则“concat()”没有任何优势。 唯一不使用“...”的原因是堆栈溢出,我同意其他答案中所说的,你不能使用“...”或“apply()”。但即使那样,“for {push()}”也比“concat()”在所有浏览器中快两倍以上,并且不会溢出。除非您需要保留旧数组,否则没有理由使用“concat()”函数。

2

2
用户6445533 的回答被接受为答案,但我觉得测试用例有点奇怪。那不像通常使用扩展运算符的方式。
为什么不能像这样做:
let newPets = [...pets, ...wishlist]

如Hashbrown所述,它不会遇到任何stackoverflow问题。此外,它可能会带来性能上的好处。

*我也正在学习ES6,如果我说错了请见谅。


2
将上述文本翻译成中文:
将问题解释为“通常情况下哪种方法更高效,以.push()为例”,看起来apply(除了MS Edge外)[仅略微]更快
这里有一份性能测试(链接),仅测试两种方法调用函数时的额外开销。
function test() { console.log(arguments[arguments.length - 1]); }
var using = (new Array(200)).fill(null).map((e, i) => (i));

test(...using);

test.apply(null, using)

我在Chrome 71.0.3578.80(官方版本)(64位)、FF 63.0.3(64位)和Edge 42.17134.1.0上进行了测试,这是我的结果,在它们自己上运行几次后。初始结果总是倾向于某一方向。

benchmark results for the three browsers

正如您所看到的,Edge 似乎对 apply 的实现比对 ... 更好(但不要试图在不同浏览器之间比较结果,我们无法从这些数据中判断 Edge 是否有更好的 apply,更差的 ...,或两者都有)。鉴于此,除非您特别针对 Edge,否则建议使用 ...,因为它更易读,特别是如果您需要将一个对象传回 apply 以用于 this
另外,也有可能取决于数组的大小,就像 @Jaromanda X 所说的那样,请自行进行测试,并根据需要更改 200
其他回答将问题解释为“哪种方法对.push()来说更好”,并专注于所解决的“问题”,仅建议使用.concat(),这基本上是标准的“为什么要用那种方式?”这可能会惹恼一些从谷歌来的人,他们不是在寻找与.push()有关的解决方案(比如Math.max或自己的自定义函数)。

-2

如果你使用的是ES2015,那么扩展运算符就是正确的方法。使用扩展运算符,你的代码看起来不那么冗长,与其他方法相比更加简洁。至于速度方面,我相信两种方法之间的差别很小。


1
请提供一个真实的测试来证明你的速度声明。在我的测试中,spread操作比Function.prototype.apply慢得多。例如:https://gist.github.com/joliss/4de65fa03d547fc1c814 - Cody Allan Taylor
无论速度如何,有时您不希望改变数组。在处理现代JavaScript或TypeScript时,这种情况下使用spread操作符是最好的选择。例如,在编写Redux reducer时。我特别指的是与_array.push()相比较的情况。concat始终可用且性能良好,但看起来不够好或者不够明确。 - Irv Lennert

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