for-in和Object.keys的区别以及不考虑继承属性的forEach

51
我在查看使用普通对象的Object.keys+forEachfor-in进行基准测试。这个测试结果显示,Object.keys+forEach要比for-in62%。但是如果您不想获取继承属性怎么办?for-in包括所有非本地继承对象,因此我们必须使用hasOwnProperty进行检查。
我尝试制作另一个基准测试,就是这样做的。但是现在,在Chrome中,for-in方法比Object.keys+forEach方法慢41%
更新:
以上测试是在Chrome中进行的。再次尝试Safari,我得到了不同的结果:Object.keys(..).forEach(..)for-in方法慢34%,奇怪。
备注:我进行基准测试的原因是为了检查在Node.js中如何运行。
问题:
1. Chrome中的jsperf结果是否适用于Node.js? 2. 发生了什么?为什么单个条件语句使for-in方法在Chrome中比Object.keys+forEach方法慢41%

你能在这里突出显示问题吗?似乎有很多需要回答的问题。 - CodingIntrigue
更新了我的问题。对于造成的困惑,我很抱歉。 - majidarif
7个回答

44

Node.js使用V8引擎,虽然我猜它不同于Chrome当前版本的V8引擎,但我认为这是衡量Node.js性能的好指标。

其次,你正在使用forEach,这在开发时非常方便,但对于每次迭代都添加一个回调函数,这是一项(相对)耗时的任务。所以,如果你关注性能,为什么不使用普通的for循环呢?

for (var i = 0, keys = Object.keys(object); i < keys.length; i++) {
    // ...
}

这可以获得最佳的表现,解决你在Safari中遇到的速度问题。

简而言之:不是条件语句,而是每次迭代调用hasOwnProperty使其与众不同。由于每次都进行函数调用,因此for...in变得更慢。


顺便说一句,在循环的条件部分调用Object.keys()并不是一个好主意,因为它会在每次迭代中被调用。 - AskarovBeknar
@AskarovBeknar,我不知道你是否指的是上面的代码片段,但正是因为这个精确的原因才没有发生那种情况。 - MaxArt
啊,对不起。实际上是在初始化部分。然后它完全正确。你说得对,使用for循环而不是foreach会加快执行速度,因为它不会在每次迭代时创建闭包,而且你可以从中断开。 - AskarovBeknar
性能测试实际上表明这样做没有明显的改进。即使移除增量部分或其他类似尝试也都是微不足道的。这基本上是你能够达到的最快速度,所以最好保持代码的可读性和可维护性。 - MaxArt
@MaxArt @Eadel 以前在每次迭代中访问length属性会显著降低循环性能,因此出现了, len=keys.length; i<len;这种写法。 - Roy Tinker
显示剩余6条评论

23

请注意:

var keys = Object.keys(obj), i = keys.length;

while(--i) {
   //
}

如果不在索引0处运行,那么其中一个属性就会被遗漏。

例如对于像["a","b","c","d"]这样的数组,只会运行d、c、b,并且你将错过"a",因为索引是0且0为假。

你需要在while检查后递减:

var keys = Object.keys(obj), i = keys.length;

while(i--) {
   //
}

3
我今天对此也很感兴趣,主要是因为我不喜欢在已知对象干净(因为它们是从对象字面量创建的)时仍然需要使用hasOwnProperty进行默认检测。无论如何,我对@styonsk的答案进行了扩展,包括更好的输出和运行多个测试并返回输出。
结论:对于Node来说这很复杂。最佳方法似乎是在nodejs v4.6.1上使用Object.keys()与数字for循环或while循环。在v4.6.1上,使用hasOwnProperty的forIn循环是最慢的方法。但是,在node v6.9.1上它是最快的,但仍然比v4.6.1上的两个Object.keys()迭代器慢。
注:这是在2013年底发布的MacBook Pro上运行的,配备16GB内存和2.4Ghz i5处理器。每个测试都占用了一个CPU核心的100%,持续时间约为500MB,并峰值达到1GB的rss。希望这能帮助某些人。
以下是我的结果,针对具有大量属性(10^6)和少量属性(50)的nodejs v6.9.1和v4.6.1运行。

Node v4.6.1具有100万属性的大对象

测试对象键名(使用while递减) 测试次数:100 总时间:57595毫秒 平均时间:575.95毫秒

测试对象键名(使用for循环) 测试次数:100 总时间:54885毫秒 平均时间:548.85毫秒

测试for-in循环 测试次数:100 总时间:86448毫秒 平均时间:864.48毫秒

Node v4.6.1具有50个属性的小对象

测试对象键名(使用while递减) 测试次数:1000 总时间:4毫秒 平均时间:0.004毫秒

测试对象键名(使用for循环) 测试次数:1000 总时间:4毫秒 平均时间:0.004毫秒

测试for-in循环 测试次数:1000 总时间:14毫秒 平均时间:0.014毫秒

Node v6.9.1具有100万属性的大对象

测试对象键名(使用while递减) 测试次数:100 总时间:94252毫秒 平均时间:942.52毫秒

测试对象键名(使用for循环) 测试次数:100 总时间:92342毫秒 平均时间:923.42毫秒

测试for-in循环 测试次数:100 总时间:72981毫秒 平均时间:729.81毫秒

Node v4.6.1具有50个属性的小对象

测试对象键名(使用while递减) 测试次数:1000 总时间:8毫秒 平均时间:0.008毫秒

测试对象键名(使用for循环) 测试次数:1000 总时间:10毫秒 平均时间:0.01毫秒

测试for-in循环 测试次数:1000 总时间:13毫秒 平均时间:0.013毫秒

以下是我运行的代码:
//Helper functions
function work(value) {
  //do some work on this value
}

function createTestObj(count) {
  var obj = {}
  while (count--) {
    obj["key" + count] = "test";
  }
  return obj;
}

function runOnce(func, obj) {
  var start = Date.now();
  func(obj);
  return Date.now() - start;
}

function testTimer(name, func, obj, count) {
  count = count || 100;
  var times = [];
  var i = count;
  var total;
  var avg;

  while (i--) {
    times.push(runOnce(func, obj));
  }

  total = times.reduce(function (a, b) { return a + b });
  avg = total / count;

  console.log(name);
  console.log('Test Count: ' + count);
  console.log('Total Time: ' + total);
  console.log('Average Time: ' + avg);
  console.log('');
}

//Tests
function testObjKeyWhileDecrement(obj) {
  var keys = Object.keys(obj);
  var i = keys.length;
  while (i--) {
    work(obj[keys[i]]);
  }
}

function testObjKeyForLoop(obj) {
  var keys = Object.keys(obj);
  var len = keys.length;
  var i;
  for (i = 0; i < len; i++) {
    work(obj[keys[i]]);
  }
}

function testForInLoop(obj) {
  for (key in obj) {
    if (obj.hasOwnProperty(key)) {
      work(obj[key]);
    }
  }
}

//Run the Tests
var data = createTestObj(50)
testTimer('testObjKeyWhileDecrement', testObjKeyWhileDecrement, data, 1000);
testTimer('testObjKeyForLoop', testObjKeyForLoop, data, 1000);
testTimer('testForInLoop', testForInLoop, data, 1000);

1
我刚刚测试了Object.keys(obj).forEach(work),它恰好位于forIn和Object.keys之间,可以与数值for和while循环完美配合。 - baetheus
1
我在这里为此编写了一个jsperf:https://jsperf.com/object-property-iteration - baetheus

1

我在@Hominiminim的代码中添加了更多的for循环,并且这里是我在Angular和Google Chrome上运行的结果(较低的值表示更好):

  • For: 1209
  • For In:1184
  • For Of (Object.entries):2115
  • For Of (Object.keys):1220
  • For Of (Object.values):1571
  • For Each (Object.entries):1776
  • For Each (Object.keys):1228
  • For Each (Object.values):1475

如果有人想要自己尝试,这里是在jsfiddle中的代码:https://jsfiddle.net/hikarii_flow/295v7sb3/

此外,这是我在jsfiddle上的结果:

  • 对于:477
  • 对于(在内部):458
  • 对于(对象的条目):1361
  • 对于(对象的键):493
  • 对于(对象的值):817
  • 对每个(对象的条目):1123
  • 对每个(对象的键):489
  • 对每个(对象的值):806

function work(value) {
    const arr = Array.from(value);
    const arr2 = arr.reverse();
    const arr3 = arr2.reverse();
    const test = arr3.indexOf('y');
}

function createTestObj(count) {
    let obj = {};

    while (count--) {
        obj["key" + count] = "test";
    }

    return obj;
}

function For(obj) {
    const start = Date.now()

    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
        work(obj[keys[i]]);
    }

    console.log("For: " + (Date.now() - start));
}

function ForIn(obj) {
    const start = Date.now()

    for (const key in obj) {
        work(obj[key]);
    }

    console.log("For In: " + (Date.now() - start));
}

function ForOfEntries(obj) {
    const start = Date.now();

    for (const [key, value] of Object.entries(obj)) {
        work(value);
    }

    console.log("For Of (entries): " + (Date.now() - start));
}

function ForOfKeys(obj) {
    const start = Date.now();

    for (const key of Object.keys(obj)) {
        work(obj[key]);
    }

    console.log("For Of (keys): " + (Date.now() - start));
}

function ForOfValues(obj) {
    const start = Date.now();

    for (const value of Object.values(obj)) {
        work(value);
    }

    console.log("For Of (values): " + (Date.now() - start));
}

function ForEachEntries(obj) {
    const start = Date.now();

    Object.entries(obj).forEach(kvp => {
        const [key, value] = kvp;
        work(value);
    });

    console.log("For Each (entries): " + (Date.now() - start));
}

function ForEachKeys(obj) {
    const start = Date.now();

    Object.keys(obj).forEach(key => {
        work(obj[key]);
    });

    console.log("For Each (keys): " + (Date.now() - start));
}

function ForEachValues(obj) {
    const start = Date.now();

    Object.values(obj).forEach(value => {
        work(value);
    });

    console.log("For Each (values): " + (Date.now() - start));
}

//Run the Tests
const data = createTestObj(1000000);
For(data);
ForIn(data);
ForOfEntries(data);
ForOfKeys(data);
ForOfValues(data);
ForEachEntries(data);
ForEachKeys(data);
ForEachValues(data);


0
今天我进行了测试。对于我的目的,获取对象键然后使用普通的for循环比使用递减while或for in循环更快。请随意更改此模板以测试不同的循环以适应您的个人情况:

//Helper functions
function work(value) {
  //do some work on this value
}

function createTestObj(count) {
  var obj = {}
  while (count--) {
    obj["key" + count] = "test";
  }
  return obj;
}

//Tests
function test_ObjKeyWhileDecrement(obj) {
  console.log("Time Started: ", new Date().getTime());
  var keys = Object.keys(obj),
    i = keys.length;
  while (i--) {
    work(obj[keys[i]]);
  }
  console.log("Time Finished: ", new Date().getTime());
}

function test_ObjKeyForLoop(obj) {
  console.log("Time Started: ", new Date().getTime());
  for (var i = 0, keys = Object.keys(obj); i < keys.length; i++) {
    work(obj[keys[i]]);
  }
  console.log("Time Finished: ", new Date().getTime());
}

function test_ForInLoop(obj) {
  console.log("Time Started: ", new Date().getTime());
  for (key in obj) {
    work(obj[key]);
  }
  console.log("Time Finished: ", new Date().getTime());
}

//Run the Tests
var data = createTestObj(1000 * 100)
console.log("Test Obj Key While Decrement Loop")
test_ObjKeyWhileDecrement(data);
console.log("Test Obj Key For Loop")
test_ObjKeyForLoop(data);
console.log("Test For In Loop")
test_ForInLoop(data);

您可能希望在实际环境中运行以进行测试,而不是在 jsfiddle 中运行。也可以尝试多个浏览器。

4
在这种情况下,您应该使用console.timeconsole.timeEnd - Rajesh

0

对于仍然关注在JS中迭代对象属性的任何人,绝对最快的方法是:

var keys = Object.keys(obj), i = keys.length;

while(--i) {
   //
}

http://jsperf.com/object-keys-foreach-vs-for-in-hasownproperty/8

对于大型对象,您可以通过不重新计算键数组的长度值(在现代浏览器优化下基本可以忽略不计)来节省一些空间,这也适用于简单的for循环情况。相比较而言,递减的while循环仍然比for循环或递增的while循环与长度上限比较要快得多。


12
预先减量检查会在索引0处打破条件。请参见答案https://dev59.com/vV8f5IYBdhLWcg3wFfT_#35117591。 - LeZuse

-1

2018年:在V8上,keys-reduce仅快了0.5%,但在FireFox上慢了2%。 - dube

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