62

From the MDN docs for the standard setPrototypeOf function as well as the non-standard __proto__ property:

Mutating the [[Prototype]] of an object, no matter how this is accomplished, is strongly discouraged, because it is very slow and unavoidably slows down subsequent execution in modern JavaScript implementations.

Using Function.prototype to add properties is the way to add member functions to javascript classes. Then as the following shows:

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

Why is foo.__proto__.bar = bar; bad? If its bad isn't Foo.prototype.bar = bar; just as bad?

Then why this warning: it is very slow and unavoidably slows down subsequent execution in modern JavaScript implementations. Surely Foo.prototype.bar = bar; is not that bad.

Update Perhaps by mutation they meant reassignment. See accepted answer.

basarat
  • 261,912
  • 58
  • 460
  • 511
  • 1
    `__proto__` is a *deprecated* non-standard property .. so that out of the way, it would be nice see answers that address the performance question directly: Why is it "..very slow and unavoidably slows down subsequent execution in modern JavaScript implementations"? – user2864740 May 22 '14 at 13:30
  • @user2864740 thanks. That was my intention. Updated question to be more explict by mentioning the new *standard* way. – basarat May 22 '14 at 13:34
  • I can't find the answer I want reference to, but IIRC modifying the prototype would require the js engine to _throw away_ all the native code created by the JIT-compiler for those objects, as the properties won't match anymore, which would slow down everything. – t.niese May 22 '14 at 13:35
  • @user2864740 agreed. Which is why I am asking why `foo.__proto__.bar = bar;` is bad. – basarat May 22 '14 at 13:35
  • 2
    @basarat I imagine they both have the same problem. It seems like the JS engine would need to "flush" any cached property resolutions or other compiled/intermediate IL for all chained (derived) objects. – user2864740 May 22 '14 at 13:36
  • @user2864740 I guess it should say mutating *after* you have instances created is bad. Then both of these methods would be equally bad and Alex's answer will apply. – basarat May 22 '14 at 13:38
  • 1
    @basarat Perhaps. Although I don't agree with that answer as it seems to skirt any issues relating to explicit mutation (one can mutate without `__proto__`, as shown) and implies that there are no such optimizations occurring (which would make any performance impact wrt. mutations non-existant). – user2864740 May 22 '14 at 13:40
  • 1
    I found the question and the corresponding answer I was looking for: [Should I put default values of attributes on the prototype to save space?](http://codereview.stackexchange.com/questions/28344/should-i-put-default-values-of-attributes-on-the-prototype-to-save-space) while not fully related I think this is one of the reasons why you shouldn't do that. – t.niese May 22 '14 at 13:47

4 Answers4

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

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

No. Both are doing the same thing (as foo.__proto__ === Foo.prototype), and both are fine. They're just creating a bar property on the Object.getPrototypeOf(foo) object.

What the statement refers to is assigning to the __proto__ property itself:

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

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

The warning at the Object.prototype page goes into more detail:

Mutating the [[Prototype]] of an object is, by the nature of how modern JavaScript engines optimize property accesses, a very slow operation

They simply state that changing the prototype chain of an already existing object kills optimisations. Instead, you're supposed to create a new object with a different prototype chain via Object.create().

I couldn't find an explicit reference, but if we consider how V8's hidden classes were implemented (and the more recent write-up), we can see what might go on here. When changing the prototype chain of an object, its internal type changes - it does not simply become a subclass like when adding a property, but is completely swapped. It means that all property lookup optimisations are flushed, and precompiled code will need to be discarded. Or it simply falls back to non-optimized code.

Some notable quotes:

  • Brendan Eich (you know him) said

    Writable _proto_ is a giant pain to implement (must serialize to cycle-check) and it creates all sorts of type-confusion hazards.

  • Brian Hackett (Mozilla) said:

    Allowing scripts to mutate the prototype of pretty much any object makes it harder to reason about the behavior of a script and makes VM, JIT, and analysis implementation more complex and buggier. Type inference has had several bugs due to mutable _proto_ and cannot maintain several desirable invariants because of this feature (i.e. 'type sets contain all the possible type objects which can realized for a var/property' and 'JSFunctions have types which are also functions').

  • Jeff Walden said:

    Prototype mutation after creation, with its erratic performance destabilization, and the impact upon proxies and [[SetInheritance]]

  • Erik Corry (Google) said:

    I don't expect big performance gains from making proto non-overwritable. In non-optimized code you have to check the prototype chain in case the prototype objects (not their identity) have been changed. In the case of optimized code you can fall back to nonoptimized code if someone writes to proto. So it wouldn't make all that much difference, at least in V8-Crankshaft.

  • Eric Faust (Mozilla) said

    When you set _proto_, not only are you ruining any chances you may have had for future optimizations from Ion on that object, but you also force the engine to go crawling around to all the other pieces of type inference (information about function return values, or property values, perhaps) which think they know about this object and tell them not to make many assumptions either, which involves further deoptimization and perhaps invalidation of existing jitcode.
    Changing the prototype of an object in the middle of execution is really a nasty sledgehammer, and the only way we have to keep from being wrong is to play it safe, but safe is slow.

outis
  • 75,655
  • 22
  • 151
  • 221
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    I think we all read the page OP linked to. What *are* those certain optimisations? – Alex W May 22 '14 at 14:08
  • According to this by *mutating* they mean *reassigning*. In that case using `fred = Object.create(Object.prototype)` should be just as bad. But they specifially say its *good* : `Instead, create the object with the desired [[Prototype]] using Object.create.`. I think you are right though. They've possibly optimized `Object.create`. – basarat May 22 '14 at 14:14
  • 2
    There is a significant performance difference between object.create and __proto__ : http://jsperf.com/proto-vs-object-create2 Thanks for your time – basarat May 22 '14 at 14:37
  • @basarat: I refrained from writing a jsperf, because those are hard to write and you don't know what exactly they test (or whether what you test is relevant in real code, or you got an easily optimisable one). The original case you linked had some good tests in the latter revisions. For your test, I've [updated it](http://jsperf.com/proto-vs-object-create2/2) with a static target prototype of new objects, which shows the difference even better. – Bergi May 22 '14 at 15:09
  • @Bergi `Object.create` is uber optimized in Mozilla but fails miserably in V8. when focusing in Node.js programming, \_\_proto\_\_ is **usually** a better call, performance wise. Express.js uses \_\_proto\_\_ because of the performance gains against Object.create – pocesar May 26 '14 at 01:05
  • Also read the first paragraph of https://mail.mozilla.org/pipermail/es-discuss/2010-April/010917.html – Bergi Apr 04 '15 at 19:36
  • Great answer, it made me better understand the whole prototype issue and knowing that simply adding methods to the prototype is fine is a nice solution in most cases. Does it mean I can also do `delete MyObject.__proto__['method'];` without any problems? – Wilt Jun 18 '16 at 21:02
  • @Wilt: No, [`delete` has its own problems](https://www.smashingmagazine.com/2012/11/writing-fast-memory-efficient-javascript/#de-referencing-misconceptions). – Bergi Jun 18 '16 at 21:06
  • FYI, the link to v8's hidden classes implemenation is broken – B T Sep 29 '17 at 05:18
  • 1
    @BT Thanks, fixed – Bergi Sep 29 '17 at 11:54
  • @Bergi Do you happen to have any insight into whether the performance is also impacted if `Object.setPrototypeOf()` is used straight after object creation and the prototype chain is not touched subsequently? The main reason for asking is that `Object.create()` cannot be used for initializing an object with methods making use of the `super` keyword. In other words, would the following be fine?: `const parent = { method() {} }; const child = { method() { super.method() } }; Object.setPrototypeOf(child, parent);` – Oliver Sieweke Jul 23 '19 at 08:10
  • 1
    @OliverSieweke While I don't have any real insight, I would expect no problems and think the `const child = Object.setPrototypeOf({ method() { super.method() } }, parent)` pattern is fine. Engines should be able to optimise this, and if they don't I'd file a feature request. As you say, it's the only way to make methods work in object literals, also it's necessary for arrays or functions with custom prototype chains. Just use it in those cases. – Bergi Jul 23 '19 at 09:13
  • @OliverSieweke That said, I'm not gonna edit my answer to *recommend* the usage of `Object.setPrototypeOf`. Also, you still may run into performance issues in some engines, so you should benchmark carefully if you care about it. I then would look into using `class` syntax as an alternative, engines do care to optimise that better since it's more popular. – Bergi Jul 23 '19 at 09:17
3

__proto__/setPrototypeOf aren't the same as assigning to the object prototype. For example, when you have a function/object with members assigned to it:

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

Everybody seem to be focusing only on the prototype, and forget that functions can have members assigned to it and instantiated after mutation. There's currently no other way of doing this without using __proto__/setPrototypeOf. Barely anyone use a constructor without the ability to inherit from a parent constructor function, and Object.create fails to serve.

And plus, that's two Object.create calls, which at the present moment, is ungodly slow in V8 (both browser and Node), which makes __proto__ a more viable choice

pocesar
  • 6,860
  • 6
  • 56
  • 88
1

Yes .prototype= is just as bad, hence the wording "no matter how it is accomplished". prototype is a pseudo object for extending the functionality at the class level. Its dynamic nature slows down script execution. Adding a function on the instance level, on the other hand, incurs far less overhead.

Schien
  • 3,855
  • 1
  • 16
  • 29
  • `Adding a function on the instance level...incurs far less overhead.` - until you have many instances. – Adam Jenkins May 22 '14 at 13:30
  • 1
    More context is needed. From my understanding of the linked resources it specifically relates to the *mutation* of the `[prototype]` object. As such, the assignment to `Fn.prototype` isn't "just as bad" because it's *copied on create*. (The question focuses on *mutating* the prototype object.) – user2864740 May 22 '14 at 13:34
-1

Here is a benchmark using node v6.11.1

NormalClass: A normal class, with the prototype non edited

PrototypeEdited: A class with the prototype edited (the test() function is added)

PrototypeReference: A class with the added prototype function test() who referer to an external variable

Results :

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)

As you can see, the prototype edited class is a way faster than the normal class. The prototype who has a variable which refer to an external one is the slowest, but that's an interesting way to edit prototypes with already instantied variable

Source :

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
    None of these examples involve changing the [[prototype]] slot of any object (by writing to `.__proto__` or calling `Object.setProtottypeOf()`, so the benchmark, while interesting, is irrelevant to the question posed. – cpcallen Feb 22 '18 at 23:02