为什么改变对象的[[prototype]]会影响性能?

63

根据 MDN 文档中的 标准 setPrototypeOf 函数 和非标准的__proto__ 属性,无论是如何完成,改变对象的 [[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;并不那么糟糕。

更新:也许他们指的是通过修改重新赋值。请参见被接受的答案。


1
__proto__是一个已弃用的非标准属性..既然这个问题已经解决了,那么直接回答性能问题会更好:为什么在现代JavaScript实现中它会"..非常慢并且不可避免地减缓后续执行"? - user2864740
2
@basarat 我想他们两个都有同样的问题。似乎JS引擎需要“刷新”所有链接(派生)对象的任何缓存属性解析或其他已编译/中间IL。 - user2864740
@user2864740 我猜应该是在创建实例之后进行突变才不好。那么这两种方法都是同样糟糕的,Alex的答案将适用。 - basarat
1
@basarat 可能吧。虽然我不同意那个答案,因为它似乎回避了与显式变异有关的任何问题(一个人可以在没有 __proto__ 的情况下进行变异,如所示),并暗示没有发生任何这样的优化(这将使任何性能影响 wrt. 变异是不存在的)。 - user2864740
1
我找到了我正在寻找的问题和相应的答案:是否应该将属性的默认值放在原型上以节省空间?虽然不完全相关,但我认为这是你不应该这样做的原因之一。 - t.niese
显示剩余3条评论
4个回答

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

// But this is okay
Foo.prototype.bar = bar;
不是的。两者都在做相同的事情(就像foo.__proto__ === Foo.prototype一样),而且两者都可以。它们只是在Object.getPrototypeOf(foo)对象上创建一个bar属性。
该语句所指的是将值分配给__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页面上的警告详细解释了:

通过改变对象的[[Prototype]],由于现代JavaScript引擎优化属性访问的方式,会导致非常缓慢的操作。

他们仅仅说明了改变已存在对象的原型链破坏优化。相反,你应该使用Object.create()来创建一个具有不同原型链的新对象。

我找不到明确的参考资料,但如果我们考虑V8的隐藏类是如何实现的(以及最近的撰写),我们就可以看出这里可能会发生什么。当更改对象的原型链时,其内部类型会发生变化——它不仅仅是像添加属性那样成为一个子类,而是完全交换了。这意味着所有的属性查找优化都被清除,预编译代码将需要被丢弃。或者它只是回退到非优化代码。
一些值得注意的引用:
  • 布兰登·艾奇(你知道他)说

    可写的 _proto_ 实现起来非常麻烦(必须序列化以进行循环检查),并且会产生各种类型混淆的危险。

  • Brian Hackett(Mozilla)说

    允许脚本修改几乎任何对象的原型,使得脚本行为更难以理解,并且使得 VM、JIT 和分析实现更加复杂和容易出错。由于可变的 _proto_ 特性,类型推断出现了多个 bug,并且无法保持多个期望的不变量(即“类型集包含可以为 var/property 实现的所有可能类型对象”和“JSFunctions 具有也是函数的类型”)。

  • Jeff Walden 说

    原型创建后的突变,导致性能不稳定,影响代理和 [[SetInheritance]]。

  • Erik Corry(Google)说

    我不认为使 proto 不可重写会带来很大的性能提升。在非优化代码中,您必须检查原型链,以防原型对象(而不是它们的标识)已更改。在优化代码的情况下,如果有人写入 proto,您可以回退到非优化代码。因此,在 V8-Crankshaft 中,这并不会带来太大的差异。

  • Eric Faust(Mozilla)说

    当您设置 _proto_ 时,不仅会破坏 Ion 对该对象未来优化的任何机会,而且还会迫使引擎在所有其他类型推断片段中爬行(例如函数返回值或属性值),告诉它们也不要做出许多假设,这涉及进一步的去优化和可能无效现有 jitcode。
    在执行过程中更改对象的原型确实是一个恶劣的破坏性操作,我们避免出错的唯一方法就是保持安全,但是安全是慢的。


1
我认为我们都阅读了OP链接的页面。那些特定的优化是什么? - Alex W
2
object.create和__proto__之间存在显著的性能差异:http://jsperf.com/proto-vs-object-create2 感谢您的时间。 - basarat
请阅读 https://mail.mozilla.org/pipermail/es-discuss/2010-April/010917.html 的第一段。 - Bergi
1
@BT 谢谢,已修复。 - Bergi
1
@OliverSieweke 尽管我没有任何实质性的见解,但我认为const child = Object.setPrototypeOf({ method() { super.method() } }, parent)这种模式应该没有什么问题。引擎应该能够对此进行优化,如果不能的话我会提出一个特性请求。正如你所说,这是使方法在对象字面量中工作的唯一方法,也对于带有自定义原型链的数组或函数是必要的。只需在这些情况下使用它即可。 - Bergi
显示剩余8条评论

3

__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(浏览器和Node)中非常慢,这使__proto__成为更可行的选择。


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

在实例级别上添加一个函数...会产生更少的开销,直到你拥有许多实例。 - Adam Jenkins
1
需要更多的上下文。根据我对链接资源的理解,它特别涉及到[prototype]对象的变异。因此,对于Fn.prototype的赋值并不是“同样糟糕”的,因为它是在创建时复制的。(问题集中在变异原型对象上。) - user2864740

-1

这是使用Node 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()

4
这些示例中都没有改变任何对象的[[prototype]]槽(通过写入.__proto__或调用Object.setPrototypeof()),因此尽管这个基准测试很有趣,但与所提出的问题无关。 - cpcallen

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