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

Mat*_*all 34 javascript optimization performance

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

for (property in object)
{
    // do stuff
}
Run Code Online (Sandbox Code Playgroud)

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

var i = myArray.length;
while (i--)
{
    // do stuff fast
}
Run Code Online (Sandbox Code Playgroud)

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

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

Dom*_*omi 31

更新2018年/ TLDR;

显然,有人把我的想法提升到了一个新的水平,并用它来加速"在对象的属性上加总"超过浏览器频谱的100倍 - 在这里找到他的jsperf:

在此输入图像描述

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

诀窍是什么?

他的代码是这样的:

var x = 0;
x += o.a;
x += o.b;
x += o.c;
// ...
Run Code Online (Sandbox Code Playgroud)

这比这快:

var x = 0;
for (var key in o) {
  x += o[key];
}
Run Code Online (Sandbox Code Playgroud)

......特别是如果我们在其中访问属性的顺序(a,b,c)匹配的顺序o隐藏类.

长解释如下:

更快的对象属性循环

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

Javascript对象

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

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

使用"常量类型的对象"而不是"字典类型"通常要快得多,因为优化器可以理解这些对象的结构.如果你对如何实现这一点感到好奇,你可能想看看Vyacheslav Egorov的博客,该博客详细介绍了V8以及其他Javascript运行时如何使用对象.Vyacheslav在此博客条目中解释了Javascript的对象属性查找实现.

循环对象的属性

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

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

一个展开的,未优化的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);
Run Code Online (Sandbox Code Playgroud)

假设你无法进行优化doSomethingWith,Amdahl定律告诉我们,当且仅当以下情况时,你可以获得很多表现:

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

我们确实可以使用,我称之为预编译的迭代器,迭代固定类型的所有对象的专用函数,即具有固定顺序的固定属性集的类型,并执行特定的查找.对所有人进行操作.迭代器doSomethingWith通过其正确的名称显式调用每个属性的回调(让我们调用它).因此,运行时总是可以使用类型的隐藏类,而不必依赖优化器的promise.以下伪代码描述了预编译迭代器如何对具有['a', 'b', 'c']给定顺序的三个属性的任何对象起作用:

doSomethingWith('a', o.a)
doSomethingWith('b', o.b)
doSomethingWith('c', o.c)
Run Code Online (Sandbox Code Playgroud)

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

性能测试

初步表现结果

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

随着更多测试的进入,我们可以在不同的优化器实现上得出一些初步结论:

  1. 即使在非常简单的循环中,预编译的迭代器通常也会表现得更好.
  2. 在IE中,这两种方法显示出最小的方差.Bravo Microsoft编写了一个不错的迭代优化器(至少对于这个特定的问题)!
  3. 在Firefox中,for ... in速度是最慢的.迭代优化器在那里做得不好.

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

JSFiddle在这里.

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

//
// 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;
};
Run Code Online (Sandbox Code Playgroud)

以下是我们如何使用它:

// ########################################################################
// 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);
Run Code Online (Sandbox Code Playgroud)

JSFiddle在这里.


Nic*_*lay 21

1)枚举属性的方法有很多种:

  • for..in (迭代对象及其原型链的可枚举属性)
  • Object.keys(obj) 返回可直接在对象上找到的可枚举属性的数组(不在其原型链中)
  • Object.getOwnPropertyNames(obj) 返回直接在对象上找到的所有属性(可枚举或不可枚举)的数组.
  • 如果您正在处理具有相同"形状"(属性集)的多个对象,那么"预编译"迭代代码可能是有意义的(请参阅此处的其他答案).
  • for..of不能用于迭代任意对象,但可以与a Map或a 一起使用,Set对于某些用例,它们都是普通对象的合适替代品.
  • ...

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

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

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

V8在某一点上使其性能退化for..in并随后修复了它.以下是for..in2017年V8内幕的帖子: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 */ }
Run Code Online (Sandbox Code Playgroud)

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

  • 您应该在for循环外声明var,这样它就不会每次尝试创建一个新的函数局部变量。 (2认同)
  • 在`for..in`循环括号内声明变量与先前或在函数顶部声明它们没有区别.由于JavaScript具有函数作用域,因此即使在括号内声明,循环内的变量也会被提升到函数的顶部. (2认同)