(for the original question)
For the facts, a performance test at jsperf and checking some things in the console are performed. For the research, the website irt.org is used. Below is a collection of all these sources put together plus an example function at the bottom.
╔═══════════════╦══════╦═════════════════╦═══════════════╦═════════╦══════════╗
║ Method ║Concat║slice&push.apply ║ push.apply x2 ║ ForLoop ║Spread ║
╠═══════════════╬══════╬═════════════════╬═══════════════╬═════════╬══════════╣
║ mOps/Sec ║179 ║104 ║ 76 ║ 81 ║28 ║
╠═══════════════╬══════╬═════════════════╬═══════════════╬═════════╬══════════╣
║ Sparse arrays ║YES! ║Only the sliced ║ no ║ Maybe2 ║no ║
║ kept sparse ║ ║array (1st arg) ║ ║ ║ ║
╠═══════════════╬══════╬═════════════════╬═══════════════╬═════════╬══════════╣
║ Support ║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 ║
╠═══════════════╬══════╬═════════════════╬═══════════════╬═════════╬══════════╣
║Array-like acts║no ║Only the pushed ║ YES! ║ YES! ║If have ║
║like an array ║ ║array (2nd arg) ║ ║ ║iterator1 ║
╚═══════════════╩══════╩═════════════════╩═══════════════╩═════════╩══════════╝
1 If the array-like object does not have a Symbol.iterator property, then trying
to spread it will throw an exception.
2 Depends on the code. The following example code "YES" preserves sparseness.
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;
}
As seen above, I would argue that Concat is almost always the way to go for both performance and the ability to retain the sparseness of spare arrays. Then, for array-likes (such as DOMNodeLists like document.body.children
), I would recommend using the for loop because it is both the 2nd most performant and the only other method that retains sparse arrays. Below, we will quickly go over what is meant by sparse arrays and array-likes to clear up confusion.
At first, some people may think that this is a fluke and that browser vendors will eventually get around to optimizing Array.prototype.push to be fast enough to beat Array.prototype.concat. WRONG! Array.prototype.concat will always be faster (in principle at least) because it is a simple copy-n-paste over the data. Below is a simplified persuado-visual diagram of what a 32-bit array implementation might look like (please note real implementations are a LOT more complicated)
Byte ║ Data here
═════╬═══════════
0x00 ║ int nonNumericPropertiesLength = 0x00000000
0x01 ║ ibid
0x02 ║ ibid
0x03 ║ ibid
0x00 ║ int length = 0x00000001
0x01 ║ ibid
0x02 ║ ibid
0x03 ║ ibid
0x00 ║ int valueIndex = 0x00000000
0x01 ║ ibid
0x02 ║ ibid
0x03 ║ ibid
0x00 ║ int valueType = JS_PRIMITIVE_NUMBER
0x01 ║ ibid
0x02 ║ ibid
0x03 ║ ibid
0x00 ║ uintptr_t valuePointer = 0x38d9eb60 (or whereever it is in memory)
0x01 ║ ibid
0x02 ║ ibid
0x03 ║ ibid
As seen above, all you need to do to copy something like that is almost as simple as copying it byte for byte. With Array.prototype.push.apply, it is a lot more than a simple copy-n-paste over the data. The ".apply" has to check each index in the array and convert it to a set of arguments before passing it to Array.prototype.push. Then, Array.prototype.push has to additionally allocate more memory each time, and (for some browser implementations) maybe even recalculate some position-lookup data for sparseness.
An alternative way to think of it is this. The source array one is a large stack of papers stapled together. The source array two is also another large stack of papers. Would it be faster for you to
- Go to the store, buy enough paper needed for a copy of each source array. Then put each source array stacks of paper through a copy-machine and staple the resulting two copies together.
- Go to the store, buy enough paper for a single copy of the first source array. Then, copy the source array to the new paper by hand, ensuring to fill in any blank sparse spots. Then, go back to the store, buy enough paper for the second source array. Then, go through the second source array and copy it while ensuring no blank gaps in the copy. Then, staple all the copied papers together.
In the above analogy, option #1 represents Array.prototype.concat while #2 represents Array.prototype.push.apply. Let us test this out with a similar JSperf differing only in that this one tests the methods over sparse arrays, not solid arrays. One can find it right here.
Therefore, I rest my case that the future of performance for this particular use case lies not in Array.prototype.push, but rather in Array.prototype.concat.
When certain members of the array are simply missing. For example:
// 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]);
Alternatively, javascript allows you to initialize spare arrays easily.
var mySparseArray = ["foo",,,,,,,,,,undefined,{},10,,,,,"bar"];
-
An array-like is an object that has at least a length
property, but was not initialized with new Array
or []
; For example, the below objects are classified as array-like.
{0: "foo", 1: "bar", length:2}
document.body.children
new Uint8Array(3)
- This is array-like because although it's a(n) (typed) array, coercing it to an array changes the constructor.
(function(){return arguments})()
Observe what happens using a method that does coerce array-likes into arrays like 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}() ));
- NOTE: It is bad practice to call slice on function arguments because of performance.
Observe what happens using a method that does not coerce array-likes into arrays like 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}() ));