为什么改变对象的[[prototype]]对性能有害?

时间:2014-05-22 13:10:42

标签: javascript performance prototype prototype-chain

来自标准 setPrototypeOf function的MDN文档以及非标准 __proto__ property

  

强烈建议不要改变对象的[[Prototype]],无论如何实现,因为它非常慢,并且不可避免地减慢了现代JavaScript实现中后续执行的速度。

使用Function.prototype添加属性是方式将成员函数添加到javascript类。然后如下所示:

function Foo(){}
function bar(){}

var foo = new Foo();

// This is bad: 
//foo.__proto__.bar = bar;

// But this is okay
Foo.prototype.bar = bar;

// Both cause this to be true: 
console.log(foo.__proto__.bar == bar); // true

为什么foo.__proto__.bar = bar;不好?如果它不好,Foo.prototype.bar = bar;同样糟糕?

然后为什么会出现这样的警告: 它非常慢,并且不可避免地会减慢现代JavaScript实现中的后续执行速度 。当然Foo.prototype.bar = bar;并不是那么糟糕。

更新也许通过突变他们意味着重新分配。见接受的答案。

4 个答案:

答案 0 :(得分:54)

// This is bad: 
//foo.__proto__.bar = bar;

// But this is okay
Foo.prototype.bar = bar;

没有。两者都在做同样的事情(如foo.__proto__ === Foo.prototype),两者都很好。他们只是在bar对象上创建Object.getPrototypeOf(foo)属性。

该陈述所指的是分配给__proto__属性本身:

function Employee() {}
var fred = new Employee();

// Assign a new object to __proto__
fred.__proto__ = Object.prototype;
// Or equally:
Object.setPrototypeOf(fred, Object.prototype);

Object.prototype page的警告更详细:

  

根据现代JavaScript引擎如何优化属性访问的性质来改变对象的[[Prototype]],这是一个非常慢的操作

他们只是声明更改已存在对象的原型链会导致优化。相反,您应该通过Object.create()创建一个具有不同原型链的新对象。

我无法找到明确的引用,但如果我们考虑如何实现V8's hidden classes,我们可以看到这里可能会发生什么。当更改对象的原型链时,其内部类型会发生变化 - 它不会像添加属性那样简单地成为子类,而是完全交换。这意味着刷新所有属性查找优化,并且需要丢弃预编译代码。或者它只是回到非优化代码。

一些值得注意的引语:

  • Brendan Eich (you know him) said

      

    可写__proto__是一个巨大的痛苦(必须序列化到循环检查),它会产生各种类型混淆的危险。

  • Brian Hackett (Mozilla) said

      

    允许脚本改变几乎任何对象的原型使得更难以推断脚本的行为并使VM,JIT和分析实现更加复杂和繁琐。类型推断由于可变的__proto__而有几个错误,并且由于这个特性而无法保持几个理想的不变量(即'类型集包含所有可能的类型对象,这些对象可以实现var / property'和' JSFunctions的类型也是函数'。

  • Jeff Walden said

      

    创建后的原型变异,其不稳定的性能不稳定,以及对代理的影响和[[SetInheritance]]

  • Erik Corry (Google) said

      

    我不希望通过使proto不可覆盖而获得巨大的性能提升。在非优化代码中,您必须检查原型链,以防原型对象(而不是其标识)已更改。在优化代码的情况下,如果有人写入proto,您可以回退到非优化代码。因此,至少在V8-Crankshaft中,它不会产生太大的影响。

  • Eric Faust (Mozilla) said

      

    当您设置__proto__时,不仅会破坏您对该对象上的Ion进行未来优化的任何机会,而且还会强制引擎转向所有其他类型推断(有关函数的信息)或者认为他们知道这个对象并且告诉他们不要做出许多假设的返回值或属性值,这涉及到进一步的去优化以及可能使现有jitcode无效。   在执行过程中更改对象的原型实际上是一个讨厌的大锤,我们必须避免错误的唯一方法是保证安全,但安全性很慢。

答案 1 :(得分:2)

__proto__ / setPrototypeOf与分配给对象原型不同。例如,当您有一个分配了成员的函数/对象时:

function Constructor(){
    if (!(this instanceof Constructor)){
        return new Constructor();
    } 
}

Constructor.data = 1;

Constructor.staticMember = function(){
    return this.data;
}

Constructor.prototype.instanceMember = function(){
    return this.constructor.data;
}

Constructor.prototype.constructor = Constructor;

// By doing the following, you are almost doing the same as assigning to 
// __proto__, but actually not the same :P
var newObj = Object.create(Constructor);// BUT newObj is now an object and not a 
// function like !!!Constructor!!! 
// (typeof newObj === 'object' !== typeof Constructor === 'function'), and you 
// lost the ability to instantiate it, "new newObj" returns not a constructor, 
// you have .prototype but can't use it. 
newObj = Object.create(Constructor.prototype); 
// now you have access to newObj.instanceMember 
// but staticMember is not available. newObj instanceof Constructor is true

// we can use a function like the original constructor to retain 
// functionality, like self invoking it newObj(), accessing static 
// members, etc, which isn't possible with Object.create
var newObj = function(){
    if (!(this instanceof newObj)){   
        return new newObj();
    }
}; 
newObj.__proto__ = Constructor;
newObj.prototype.__proto__ = Constructor.prototype;
newObj.data = 2;

(new newObj()).instanceMember(); //2
newObj().instanceMember(); // 2
newObj.staticMember(); // 2
newObj() instanceof Constructor; // is true
Constructor.staticMember(); // 1

每个人似乎只关注原型,忘记了函数可以将成员分配给它并在变异后实例化。目前没有其他方法可以在不使用__proto__ / setPrototypeOf的情况下执行此操作。几乎没有人使用构造函数而无法从父构造函数继承,Object.create无法提供服务。

而且,这是两个Object.create次调用,目前在V8(浏览器和节点)中调用速度都很慢,这使得__proto__成为更可行的选择

答案 2 :(得分:1)

是.prototype =同样糟糕,因此措辞“无论如何完成”。 prototype是一个伪对象,用于在类级别扩展功能。它的动态性质会减慢脚本执行速度。另一方面,在实例级别添加函数会产生更少的开销。

答案 3 :(得分:0)

以下是使用节点v6.11.1

的基准测试

NormalClass :普通类,未编辑原型

PrototypeEdited :编辑了原型的类(添加了test()函数)

PrototypeReference :添加了原型函数test()并引用外部变量的类

结果:

NormalClass x 71,743,432 ops/sec ±2.28% (75 runs sampled)
PrototypeEdited x 73,433,637 ops/sec ±1.44% (75 runs sampled)
PrototypeReference x 71,337,583 ops/sec ±1.91% (74 runs sampled)

正如您所看到的,原型编辑的类比普通类快。具有引用外部变量的变量的原型是最慢的,但这是使用已经变化的变量编辑原型的有趣方式

来源:

const Benchmark = require('benchmark')
class NormalClass {
  constructor () {
    this.cat = 0
  }
  test () {
    this.cat = 1
  }
}
class PrototypeEdited {
  constructor () {
    this.cat = 0
  }
}
PrototypeEdited.prototype.test = function () {
  this.cat = 0
}

class PrototypeReference {
  constructor () {
    this.cat = 0
  }
}
var catRef = 5
PrototypeReference.prototype.test = function () {
  this.cat = catRef
}
function normalClass () {
  var tmp = new NormalClass()
  tmp.test()
}
function prototypeEdited () {
  var tmp = new PrototypeEdited()
  tmp.test()
}
function prototypeReference () {
  var tmp = new PrototypeReference()
  tmp.test()
}
var suite = new Benchmark.Suite()
suite.add('NormalClass', normalClass)
.add('PrototypeEdited', prototypeEdited)
.add('PrototypeReference', prototypeReference)
.on('cycle', function (event) {
  console.log(String(event.target))
})
.run()