在Javascript中迭代对象属性的最快方法是什么?

时间:2009-10-15 16:40:35

标签: javascript performance optimization

我知道我可以遍历对象的属性,如下所示:

for (property in object)
{
    // do stuff
}

我也知道在Javascript中迭代数组的最快方法是使用递减的while循环:

var i = myArray.length;
while (i--)
{
    // do stuff fast
}

我想知道是否有类似于减少while循环的东西来迭代对象的属性。

编辑:只关注与可枚举性有关的答案 - 我不是。

8 个答案:

答案 0 :(得分:27)

更新2018年/ TLDR;

显然,有人把我的想法提升到了一个新的水平,并用它来加速对对象的属性进行总结。在浏览器范围内超过100倍 - find his jsperf here

enter image description here

粉红色的条形代表他"预编译的总和"只是将所有其他方法和操作留在尘埃中的方法。

有什么诀窍?

他的代码做到了这一点:

var x = 0;
x += o.a;
x += o.b;
x += o.c;
// ...

比这更快:

var x = 0;
for (var key in o) {
  x += o[key];
}

...尤其是如果我们访问属性的顺序(abc)与o' s中的顺序相匹配{3}}

长期解释如下:

更快的对象属性循环

首先让我说,for ... in循环很好,你只想在具有大量CPU和RAM使用率的性能关键代码中考虑这一点。通常,你应该花更多时间在你身上。但是,如果你是一个表演怪胎,你可能会对这个近乎完美的选择感兴趣:

Javascript对象

通常,JS对象有两种用例:

  1. " Dictionaries",a.k.a"关联数组"是具有不同属性集的通用容器,由字符串键索引。
  2. "常数对象" (所谓的hidden class始终相同)具有固定顺序的固定属性集。是! - 虽然标准不保证任何订单,但现代VM实施都有(隐藏)订单,以加快速度。正如我们稍后探讨的那样,始终保持这种秩序至关重要。
  3. 使用"常数类型的对象"而不是"字典类型"因为优化器理解这些对象的结构,所以通常要快得多。如果您对如何实现这一点感到好奇,那么您可能需要查看hidden class,其中详细介绍了Vyacheslav Egorov's blog以及其他Javascript运行时间,使用对象。 V8

    循环对象的属性

    默认for ... in无疑是迭代所有对象属性的好选择。但是,for ... in可能会将您的对象视为具有字符串键的字典,即使它具有隐藏类型。在这种情况下,在每次迭代中,您都有字典查找的开销,通常将其实现为Vyacheslav explains Javascript's object property look-up implementation in this blog entry。在许多情况下,优化器足够智能以避免这种情况,并且性能与您的属性的持续命名相同,但是根本无法保证。通常情况下,优化器无法帮助您,并且您的循环运行速度会比它应该慢很多。最糟糕的是,有时这是不可避免的,特别是如果你的循环变得更复杂。优化器并不那么聪明(还有!)。以下伪代码描述了for ... in在慢速模式下的工作方式:

    for each key in o:                                // key is a string!
        var value = o._hiddenDictionary.lookup(key);  // this is the overhead
        doSomethingWith(key, value);
    

    展开的,未经优化的for ... in循环,循环遍历具有三个属性的对象[' a',''' c'给定顺序,如下所示:

    var value = o._hiddenDictionary.lookup('a');
    doSomethingWith('a', value);
    var value = o._hiddenDictionary.lookup('b');
    doSomethingWith('b', value);
    var value = o._hiddenDictionary.lookup('c');
    doSomethingWith('c', value);
    

    假设您无法优化doSomethingWithhashtable lookup告诉我们,当且仅在以下情况下您可以获得很多表现:

    1. doSomethingWith已经非常快(与字典查找的成本相比)和
    2. 你实际上可以摆脱字典查找开销。
    3. 我们确实可以使用我所说的预编译的迭代器来删除该查找,这是一个专用函数,它迭代固定类型的所有对象,即具有固定集的类型固定订单的属性,并对所有这些属性执行特定操作。迭代器通过其正确的名称显式调用每个属性上的回调(让我们称之为doSomethingWith)。因此,运行时总是可以使用类型Amdahl's law,而不必依赖优化器的承诺。以下伪代码描述了预编译迭代器如何适用于具有给定顺序的三个属性['a', 'b', 'c']的任何对象:

      doSomethingWith('a', o.a)
      doSomethingWith('b', o.b)
      doSomethingWith('c', o.c)
      

      没有开销。我们不需要查看任何内容。编译器已经可以使用隐藏的类型信息轻松地计算每个属性的确切内存地址,并且它甚至使用最缓存友好的迭代顺序。这也是(非常接近)使用for...in和完美优化器可以获得的最快代码。

      性能测试

      preliminary performance results

      hidden class表明预编译迭代器方法比标准for ... in循环快得多。请注意,加速很大程度上取决于对象的创建方式和循环的复杂性。由于此测试只有非常简单的循环,因此您有时可能不会观察到很多加速。但是,在我自己的一些测试中,我能够看到预编译迭代器的速度提高了25倍;或者更确切地说是for ... in循环的显着减慢,因为优化器无法摆脱字符串查找。

      使用This jsperf,我们可以就不同的优化器实现得出一些初步结论:

      1. 预编译的迭代器通常表现得更好,即使在非常简单的循环中也是如此。
      2. 在IE中,这两种方法显示的差异最小。 Bravo Microsoft编写了一个不错的迭代优化器(至少对于这个特定的问题)!
      3. 在Firefox中,for ... in是最慢的。迭代优化器在那里做得不好。
      4. 然而,测试有一个非常简单的循环体。我仍然在寻找一个测试用例,其中优化器永远无法在所有(或几乎所有)浏览器中实现持续索引。非常欢迎任何建议!

        代码

        more tests coming in

        以下compileIterator函数为任何类型的(简单)对象预编译迭代器(暂时忽略嵌套属性)。迭代器需要一些额外的信息,表示它应迭代的所有对象的确切类型。这种类型信息通常可以表示为精确顺序的字符串属性名称数组,declareType函数用于创建方便的类型对象。如果您想查看更完整的示例,请参阅JSFiddle here

        //
        // Fast object iterators in JavaScript.
        //
        
        // ########################################################################
        // Type Utilities (define once, then re-use for the life-time of our application)
        // ########################################################################
        
        /**
          * Compiles and returns the "pre-compiled iterator" for any type of given properties.
          */
        var compileIterator = function(typeProperties) {
          // pre-compile constant iteration over object properties
          var iteratorFunStr = '(function(obj, cb) {\n';
          for (var i = 0; i < typeProperties.length; ++i) {
            // call callback on i'th property, passing key and value
            iteratorFunStr += 'cb(\'' + typeProperties[i] + '\', obj.' + typeProperties[i] + ');\n';
          };
          iteratorFunStr += '})';
        
          // actually compile and return the function
          return eval(iteratorFunStr);
        };
        
        // Construct type-information and iterator for a performance-critical type, from an array of property names
        var declareType = function(propertyNamesInOrder) {
          var self = {
            // "type description": listing all properties, in specific order
            propertyNamesInOrder: propertyNamesInOrder,
        
            // compile iterator function for this specific type
            forEach: compileIterator(propertyNamesInOrder),
        
            // create new object with given properties of given order, and matching initial values
            construct: function(initialValues) {
              //var o = { _type: self };     // also store type information?
              var o = {};
              propertyNamesInOrder.forEach((name) => o[name] = initialValues[name]);
              return o;
            }
          };
          return self;
        };
        

        以下是我们如何使用它:

        // ########################################################################
        // Declare any amount of types (once per application run)
        // ########################################################################
        
        var MyType = declareType(['a', 'b', 'c']);
        
        
        // ########################################################################
        // Run-time stuff (we might do these things again and again during run-time)
        // ########################################################################
        
        // Object `o` (if not overtly tempered with) will always have the same hidden class, 
        // thereby making life for the optimizer easier:
        var o = MyType.construct({a: 1, b: 5, c: 123});
        
        // Sum over all properties of `o`
        var x = 0;
        MyType.forEach(o, function(key, value) { 
          // console.log([key, value]);
          x += value; 
        });
        console.log(x);
        

        jsperf entry

答案 1 :(得分:20)

1)有许多不同的枚举属性的方法:

  • for..in(迭代对象及其原型链的可枚举属性)
  • Object.keys(obj)返回可直接在对象上找到的可枚举属性的数组(不在其原型链中)
  • Object.getOwnPropertyNames(obj)返回直接在对象上找到的所有属性(可枚举或不可枚举)的数组。
  • 如果你正在处理具有相同“形状”(属性集)的多个对象,那么“预编译”迭代代码可能是有意义的(参见the other answer here)。
  • for..of不能用于迭代任意对象,但可以与MapSet一起使用,它们都是适合某些特定用途的普通对象的替代品 - 例。
  • ...

也许如果你说出原来的问题,有人可能会提出一种优化方法。

2)我发现很难相信实际的枚举比你对循环体中的属性做的更多。

3)您没有指定您正在开发的平台。答案可能取决于它,可用的语言功能也取决于它。例如。在大约2009年的SpiderMonkey(Firefox JS解释器)中,如果您确实需要值而不是键,则可以使用for each(var x in arr)docs)。它比for (var i in arr) { var x = arr[i]; ... }快。

V8在某个时刻regressed the performance of for..in and subsequently fixed it。以下是2017年V8中for..in内幕的帖子:https://v8project.blogspot.com/2017/03/fast-for-in-in-v8.html

4)您可能只是没有将它包含在您的代码段中,但更快的方法是进行for..in迭代,以确保您在循环中使用的变量在包含循环的函数内声明,即:

//slower
for (property in object) { /* do stuff */ }

//faster
for (var property in object) { /* do stuff */ }

5)与(4)相关:在尝试优化Firefox扩展时,我曾经注意到将紧密循环提取到单独的函数中可以改善其性能(link)。 (显然,这并不意味着你应该总是这样做!)

答案 2 :(得分:2)

在JavaScript 1.7+中明确使用Iterator可能会更快或更慢。当然,这只会迭代对象的自己的属性。将ex instanceof StopIteration替换为ex === StopIteration时,catch语句也可能更快。

var obj = {a:1,b:2,c:3,d:4,e:5,f:6},
   iter = new Iterator(obj, true);

while (true) {
    try {
        doSomethingWithProperty(iter.next());
    } catch (ex if (ex instanceof StopIteration)) {
        break;
    }
}

答案 3 :(得分:2)

您也可以使用Object.getOwnPropertyNames来获取对象的键。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames

nextShort

答案 4 :(得分:1)

for/in循环是枚举Javascript对象属性的最佳方法。应该理解,这只会循环通过“可枚举的”属性,而不是特定的顺序。并非所有属性都是可枚举的。通过您自己的Javascript代码以编程方式添加的所有属性/方法都是可枚举的,但是继承的预定义属性/方法(例如toString)通常不会被枚举。

你可以像这样检查可枚举性......

var o = new Object();
alert(o.propertyIsEnumerable("toString"));

答案 5 :(得分:0)

根据定义,对象的属性是无序的。缺乏秩序意味着没有“前锋”,因此也没有“倒退”。

答案 6 :(得分:0)

如果您不知道属性的名称,for..in是枚举它们的好方法。如果你这样做,你最好使用明确的解除引用。

答案 7 :(得分:0)

Object.keys()与传统的for循环耦合以遍历键,并通过查找值来执行所有其他技术。这是全面的性能比较。

https://gists.cwidanage.com/2018/06/how-to-iterate-over-object-entries-in.html