(关于原始问题)
为了获得事实,对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代表Array.prototype.concat,而#2代表Array.prototype.push.apply。让我们使用一个类似的JSperf测试来检验它,唯一的区别在于这个测试针对的是稀疏数组而不是实数组。您可以在这里找到它:这里。
因此,我论断这种特定用例的性能未来不在于Array.prototype.push,而在于Array.prototype.concat。
当数组的某些成员被简单地省略时。例如:
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;
console.log(slice.call(["not an array-like, rather a real array"]));
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 = [];
console.log(empty.concat(["not an array-like, rather a real array"]));
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}() ));