Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is mutating the [[prototype]] of an object bad for performance?

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.

like image 714
basarat Avatar asked May 22 '14 13:05

basarat


People also ask

Why is mutating objects bad?

Mutation may lead to unexpected and hard-to-debug issues, where data becomes incorrect somewhere, and you have no idea where it happens. Mutation makes code harder to understand: at any time, an array or object may have a different value, so we need to be very careful when reading the code.

Why is mutating bad JavaScript?

Key takeaways about object mutationMutating older versions of state causes all sorts of headaches and hard-to-understand bugs. Instead of recognizing the nuance, developers will often avoid mutation entirely anywhere within React code. Mutation is an excellent tool when its usage is clearly communicated.

What does mutating an object mean?

To mutate means to change in form or nature. Something that's mutable can be changed, while something that's immutable cannot be changed. To understand mutation, think of the X-Men.


2 Answers

__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

like image 43
pocesar Avatar answered Oct 23 '22 07:10

pocesar


// 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 are implemented, 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.

like image 127
Bergi Avatar answered Oct 23 '22 06:10

Bergi