.forEach和Object.keys().forEach在稀疏数组上的性能差异

3
告诉我如果我说错了:适用于稀疏数组的是array.forEach(callbackFunction)。它不执行零到数组长度之间每个索引的callbackFunction,而只针对实际in数组中的键执行。(告诉我是否错误) 这些键正好是Object.keys(array)将给我的。因此(告诉我为什么我错了),如果在array本身上调用.forEach方法或者在Object.keys(array)上调用,就不应该有任何差别。所以,为什么地球上会有这样的性能差异——好像在一个情况下,从零到长度的一个巨大无意义的循环会被执行,但在另一个情况下却不会被执行。

演示性能差异的代码片段:

function doNothing(){}
CONSOLE = document.getElementById('console');

arr = [];
arr[49888999] = 42;

start = performance.now();
arr.forEach(doNothing);
duration1 = performance.now() - start;

start = performance.now();
Object.keys(arr).forEach(doNothing);
duration2 = performance.now() - start;

CONSOLE.textContent = [duration1, duration2].join('\n');
<pre id='console'></pre>

代码片段展示了回调函数在两种情况下仅被调用一次

console1 = document.getElementById('console1');
console2 = document.getElementById('console2');
function doNothingVerbose1(){
  console1.textContent = 1 + (+console1.textContent);
}
function doNothingVerbose2(){
  console2.textContent = 1 + (+console2.textContent);
}

arr = [];
arr[49888999] = 42;

start = performance.now();
arr.forEach(doNothingVerbose1);
duration1 = performance.now() - start;

start = performance.now();
Object.keys(arr).forEach(doNothingVerbose2);
duration2 = performance.now() - start;

console.log(duration1, duration2);
~~~~~ 1 ~~~~~
<pre id='console1'>0</pre>
~~~~~ 2 ~~~~~
<pre id='console2'>0</pre>

更新

我刚刚进行了一项测试,以找出上述arr=[];arr[49888999]=42;是否为实际的稀疏数组,即相比于使用arr=new Array(49889000)具有更小的内存占用。是的,事实确实如此。在循环中执行这种操作数百次,稀疏版本需要几秒钟但不会崩溃,但new Array(5000万)版本会使fiddle崩溃。因此,如果它没有被存储为引擎中的“普通C ++数组”,那么引擎必须“拥有”该数组的Object.keys,为什么引擎不充分利用它呢?我可能对JS引擎的工作过于简单化了;说它必须“拥有”Object.keys是错误的,因为它以某种方式支持我们的变量arr的“稀疏”数组实现吗?也许实际在浏览器/ JS引擎上工作的人可以在这里提供一些帮助。

jsperf上的上述测试


2
这是JS引擎的实现细节。根据环境使用不同的JS引擎,它们使用不同的优化策略,因此对于您的问题没有通用答案。但我应该指出,仅因为回调未针对空索引调用并不意味着引擎不会迭代它。毕竟,它必须以某种方式确定它是否为空。 - Lennholm
2
forEach仍然必须迭代从0到.length的所有数字值。例如,如果您执行了Object.defineProperty(arr,'42',{enumerable:false}),它仍应通过此非枚举的42索引,并且如果您执行了arr [“foo”] = bar,则应忽略此foo属性。 - Kaiido
1
好的,只需要 a = []; a.foo = "bar"; 就可以了。 - Kaiido
2
我已经给出了这样的一个例子:Object.defineProperty( arr, '42', { enumerable: false } ) https://jsfiddle.net/burt241f/ - Kaiido
1
在数组上使用 Object.keys() 看起来对我来说像是一种反模式。如果我发现自己处于这种情况下,我可能会使用普通对象而不是稀疏数组。 - Lennholm
显示剩余11条评论
2个回答

2

好的,好的,好的 - 所以这只是一个人必须要接受的事情;我不想听到那个,但那确实是正确的答案。

我将继续不阅读规格说明,并有时感到困惑。不,我不建议这种行为,这只是我的方式。在控制台上尝试它对我来说更有意义,而规格说明往往会让我入睡。值得庆幸的是,人们是不同的,不是每个人都像那样。

也许一个更有趣的问题是如何在实践中处理这种现象。例如,如果我必须处理一个“稀疏数组”,就像“51472产品的2个项目和81369产品的1个项目”一样,我将使用一个带有51472和81369键的对象({}),而不是数组([])。

仅仅因为所有键恰好都是非负整数而将其变成数组是一个坏主意过去一万年中最糟糕的主意 - 因为你随后有了FALSE FRIEND,即.forEach

2个相关问题:

为什么允许在JavaScript中创建稀疏数组?

JavaScript中稀疏数组的使用情况有哪些?


0
因为在一个情况下,会执行从零到长度的巨大无意义循环,而在另一个情况下则不会。
根据ECMA文档
  1. .forEach方法将通过其.length属性循环遍历所有数组元素。
  2. 传递给.forEach的回调仅在元素不为空时才会被调用。

为了演示这一点,您可以简单地执行以下操作:

function doNothing(){}
let perf;


console.log('Array with 50 million length and 1 non-empty element:');
const a = [];
a[49999999] = 'a';
console.log('a.length:', a.length);

perf = performance.now();
a.forEach(doNothing);
console.log('a:', performance.now() - perf);
console.log('');


console.log('Array with 0 length:');
const b = [];
b.foo = 'a';
console.log('b.length:', b.length);

perf = performance.now();
b.forEach(doNothing);
console.log('b:', performance.now() - perf);
console.log('');


console.log('Array with 50 million length and 0 non-empty element:');
const c = [];
c.length = 50000000;
console.log('c.length:', c.length);

perf = performance.now();
c.forEach(doNothing);
console.log('c:', performance.now() - perf);


3
引擎可能会为这种情况添加快速路径,但由于有更紧急的问题需要解决,所以目前还没有处理。目前Array.prototype.forEach的实现已经足够复杂了... - jmrk

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