【问题标题】:What's the fastest way to iterate over an object's properties in Javascript?在 Javascript 中迭代对象属性的最快方法是什么?
【发布时间】:2023-03-23 22:38:01
【问题描述】:

我知道我可以像这样迭代对象的属性:

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

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

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

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

编辑:只是关于与可枚举性有关的答案的一句话-我不是。

【问题讨论】:

  • 这听起来像是错位/过早的优化...您确定这是您的代码中需要优化的部分吗?
  • 没有一个“需要”优化。我正在处理 10k 到 20k 个对象的集合,所以越快越好。
  • 记得检查 hasOwnProperty(property) 这样你就不会对原型的成员进行操作(当然,除非你愿意)。
  • @DDaviesBrackett for/in 循环不会枚举继承的属性/方法
  • @DDaviesBrackett, @Josh Stodola:for-in 不会迭代内置属性和方法(特别是那些在 ECMAScript 规范中声明为“DontEnum”的属性和方法),但它会 i> 包括已添加到原型的继承成员,因为无法将脚本添加的成员声明为“DontEnum”。请注意,Crockford 的示例是添加到 String.prototype 的方法,而不是 String.prototype 的内置属性

标签: javascript performance optimization


【解决方案1】:

更新/TLDR;

事实证明,这种方法使ajv 成为最快的 JSON 验证器库。

另外 - 有人把我的想法提升到了一个新的水平,并用它在整个浏览器范围内将“对象属性求和”速度提高了 100 倍以上 - find his jsperf 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)与ohidden class 中的顺序相匹配。

详细解释如下:

更快的对象属性循环

首先让我说,for ... in 循环很好,您只想在具有大量 CPU 和 RAM 使用的性能关键代码中考虑这一点。通常,您应该花时间在更重要的事情上。但是,如果您是性能狂,您可能会对这种近乎完美的替代方案感兴趣:

Javascript 对象

一般来说,JS 对象有两种用例:

  1. “字典”,也称为“关联数组”,是具有不同属性集的通用容器,由字符串键索引。
  2. “常量类型的对象”(所谓的hidden class 始终相同)具有一组固定顺序的固定属性。是的! - 虽然标准不保证任何顺序,但现代 VM 实现都有一个(隐藏的)顺序,以加快速度。正如我们稍后将探讨的那样,始终保持这种秩序至关重要。

使用“常量类型的对象”而不是“字典类型”通常要快得多,因为优化器了解这些对象的结构。如果您对如何实现这一点感到好奇,您可能想查看Vyacheslav Egorov's blog,它揭示了V8 以及其他Javascript 运行时如何处理对象的大量信息。 Vyacheslav explains Javascript's object property look-up implementation in this blog entry.

循环遍历对象的属性

默认的for ... in 肯定是迭代对象所有属性的好选择。但是,for ... in 可能会将您的对象视为带有字符串键的字典,即使它具有隐藏类型。在这种情况下,在每次迭代中,您都有字典查找的开销,这通常实现为hashtable lookup。在许多情况下,优化器足够聪明,可以避免这种情况,并且性能与属性的常量命名相当,但根本无法保证。很多时候,优化器不能帮助你,你的循环运行速度会比它应该运行的慢很多。最糟糕的是,有时这是不可避免的,尤其是当您的循环变得更加复杂时。优化器只是没有那么聪明(还没有!)。以下伪代码描述了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', 'b', '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);

假设您无法优化doSomethingWithAmdahl's law 告诉我们,当且仅当:

  1. doSomethingWith 已经非常快了(与字典查找的成本相比)并且
  2. 您实际上可以摆脱字典查找开销。

我们确实可以使用我所说的预编译的迭代器来摆脱这种查找,这是一个迭代固定类型的所有对象的专用函数,即具有固定集合的类型固定顺序的属性,并对所有这些属性执行特定的操作。该迭代器通过其正确名称显式调用每个属性的回调(我们称之为doSomethingWith)。结果,运行时总是可以使用类型的hidden class,而不必依赖优化器的承诺。以下伪代码描述了预编译迭代器如何按给定顺序对具有三个属性['a', 'b', 'c'] 的任何对象起作用:

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

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

性能测试

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

通过more tests coming in,我们可以对不同的优化器实现得出一些初步结论:

  1. 预编译的迭代器通常性能要好得多,即使在非常简单的循环中也是如此。
  2. 在 IE 中,这两种方法的差异最小。 Bravo Microsoft 编写了一个不错的迭代优化器(至少对于这个特定问题)!
  3. 在 Firefox 中,for ... in 是最慢的。那里的迭代优化器做得不好。

但是,测试有一个非常简单的循环体。我仍在寻找一个测试用例,其中优化器永远无法在所有(或几乎所有)浏览器中实现恒定索引。非常欢迎任何建议!

代码

JSFiddle here.

以下compileIterator 函数为任何类型的(简单)对象预编译迭代器(暂时不考虑嵌套属性)。迭代器需要一些额外的信息,代表它应该迭代的所有对象的确切类型。这样的类型信息通常可以表示为字符串属性名称的数组,具有精确的顺序,declareType 函数使用它来创建方便的类型对象。如果想看更完整的例子,请参考jsperf entry

//
// 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);

JSFiddle here.

【讨论】:

  • 我很想知道这种推理方式如何在非 Chrome 浏览器中发挥作用。
  • @MattBall 我通过对测试结果的第一次分析更新了我的答案。看起来不错!
  • 更新:我终于清理了代码并添加了JSFiddle,以便更容易上手:)
  • 请记住,虽然这会降低 CPU 成本,但会增加内存成本。 eval'd 函数可能包含大量操作,如果它是为一个巨大的对象编译的。
  • @GershomMaes 是的,这涉及到一些较小的内存成本。但是关于你的第二个参数,不要忘记:eval 部分是 "offline" computational cost (即你在关键代码路径开始之前执行一次,或者第一次使用它),而使用编译函数是只有(快得多)“在线”计算成本(确实会影响您的关键路径的成本)。
【解决方案2】:

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) 您可能只是没有将它包含在您的 sn-p 中,但是进行for..in 迭代的更快方法是确保您在循环中使用的变量在包含循环的函数中声明,即:

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

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

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

【讨论】:

  • 您应该在 for 循环之外声明 var,这样它就不会每次都尝试创建一个新的函数局部变量。
  • 是什么让你这么说?这违背了我对“var”的直观理解(它不是在运行时“执行”,而是在函数开始执行之前扫描),我在规范中没有看到任何关于此的内容。所以我相信,如果在某些引擎中确实如此,那将是可以(并且应该)在引擎中修复的东西。
  • 我同意。 “除了使用 for..in 之外,没有其他方法可以枚举属性” - 在 2009 年甚至不是这样。Pre-compilation to the rescue (of performance)
  • for..in 循环括号内声明变量与之前或在函数顶部声明它们没有区别。由于 JavaScript 具有函数作用域,循环内的变量会被提升到函数的顶部,即使它是在括号内声明的。
  • @Nickolay 这个问题太笼统了,当然,尽管 .keys 和 getOwnPropertyNames 有很大的不同,因为一个不迭代不可枚举的属性而其他的。此外,这个论坛每天都会被访问,尽管它的答案是旧的,所以现在我们有责任保持它的正确性。顺便说一句,现在答案已经完成了
【解决方案3】:

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

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

var obj = {a:"a",b:"b"}
///{a: "a", b: "b"}
var keys = Object.getOwnPropertyNames(a)
///(2) ["a", "b"]

【讨论】:

  • 奇怪这与Object.keys()有何不同?
  • Keys 只获取可枚举的属性,getOwnPropertyNames 获取所有属性(除了像 proto 这样的隐藏属性)
【解决方案4】:

Object.keys() 与传统的 for 循环相结合以迭代键,并查找值执行所有其他技术。这是一个全面的性能比较。

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

【讨论】:

  • 感谢您提供清晰详细文章的链接。正是我想要的:)
【解决方案5】:

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

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

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

【讨论】:

    【解决方案6】:

    在 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;
        }
    }
    

    【讨论】:

    • 抛出的异常会减慢循环尾部的速度。这可能值得对非常大的对象/字典进行性能测试......
    • 幸运的是,V8 最近发布了期待已久的 try-catch 循环优化。这使得代码在 try/catch 内部和外部一样快。
    【解决方案7】:

    如果您不知道属性的名称,for..in 是枚举它们的好方法。如果这样做,最好使用显式取消引用。

    【讨论】:

      【解决方案8】:

      根据定义,对象的属性是无序的。无序意味着没有“前进”,也就没有“后退”。

      【讨论】:

      • 我不在乎订单。 “类似于减少 while 循环”我的意思是在速度提高方面类似。
      • 实际上定义了对象属性的顺序——它是加法的顺序。原型链上的属性顺序变得更加粗糙。
      • @olliej - 不,ECMAScript 规范中没有明确定义属性的枚举顺序。它依赖于实现,并且可能在不同对象之间有所不同。不应普遍依赖某些浏览器中某些对象的观察顺序
      • 我正要说它是在 ES5 中定义的,但实际上它在 4 月的最终草案中仍未定义(第 12.6.4 节 for-in 声明)
      • @ollie, @Tim Down:值得一提的是,不能保证对象的属性在两次连续迭代中以相同的顺序返回,即使没有添加、删除或修改任何属性。我不知道有一种实现是如此令人讨厌,以至于每次只以不同的随机生成序列返回事物,但这种实现实际上符合 ECMAScript 规范。任何依赖插入顺序为枚举顺序的代码都会被破坏;依赖未指定的行为是一个错误。
      猜你喜欢
      • 2012-03-03
      • 1970-01-01
      • 1970-01-01
      • 2012-11-14
      • 2017-03-28
      • 2015-10-22
      • 2023-01-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多