In my previous post about descriptors I've left you with a couple of uncommented benchmarks, one comparing
Object.defineProperty
VS direct property set, and a second one comparing Object.defineProperties
VS literal Object
notation.I am sorry to tell you that's still too early to properly comment those benchmarks, and while I am promising you I will tell you how good or bad those look later on, I'd like to put in your face another benchmark, ignoring all powerful daily basis computers are surfing the net, focusing and analyzing "the worst Hardware" I could find these days out there: an old Android 2.x, one of the first FirefoxOS phones, webOS 2 on Palm Pre 2, and a Samsung Omnia 7 with Windows Phone 7.x and IE9 Mobile
About Descriptors And Prototypal Inheritance
If you think that around 1584 classes definitions per second is the problem, and only in the worst Hardware scenario, you should probably have a quick chat with yourself about the architecture you are using for your Software, and what kind of Hardware you are actually targeting.Just as a gently reminder, this is the Hardware used for the Apollo 11 mission to the Moon:
The Apollo AGC itself is a piece of computing history, it was developed by the MIT Instrumentation Laboratory and it was a quite amazing piece of hardware in the 1960s. It was the first computer to use integrated circuits (ICs), running at 1 Mhz it offered four 16-bit registers, 4K words of RAM and 32K words of ROM. The AGC mutlitasking operating system was called the EXEC, it was capable of executing up to 8 jobs at a time.Accordingly, ignoring arguments that cannot be proved in the real-world, the rest of the post will be focused on how to use descriptors together with prototypes and "classes".
1st Fact: Zero Performance Impact!
Just to be clear upfront about this topic, usingObject.defineProperties(Class.prototype, descriptors)
does not affect instances creation performance, in Mobile, as in Desktop browsers.The rest of this post will rather be about how
descriptors
work and can be useful so if you are looking for other benchmarks, feel free to come back at Part 3 :P
About Multiple Descriptors Objects
When we use a second argument forObject.create(proto, descriptors)
or just the descriptors
object through Object.defineProperties(object, descriptors)
we are actually performing a classic for/in
loop, Douglas Crockford and his JSLint style ... yeah, that one that made your for/in
loops slower than they could have been since ever:
// simulating defineProperties function defineProperties( object, descriptors ) { for (var key in descriptors) { if (descriptors.hasOwnProperty(key)) { Object.defineProperty( object, key, descriptors[key]); } } }OK, to be fair, this might be optimized in core as
Object.keys(object)
could be, but still many operations in JavaScript world, native or not, are doomed because of ... guess what? Descriptors were missing, so nobody could define undesired properties as not-enumerable
for past 12 years ... do you still think descriptors are such bad news?
The Old ES3 Classes Pattern
One of the most disturbing patterns I keep finding in modern blog posts and even books is the following:function Person(){} Person.prototype = { constructor: Person, // and now ... whatever .. i mean ... // DO YOU REALIZE WHAT // YOU JUST DID ALREADY ? };Since developers are keen to performance and usually most relevant are about CPU rather than RAM usage and/or GC, here a little and quick breakdown of the most broken pattern you've ever seen and used in your own code (I bet so, since I've done that too!)
// defining the class function Person(){}Let's start saying that every function has its own prototype object by default, assigned since its creation, and every prototype has a property called
constructor
which is not enumerable, but configurable and writable as every other thing that comes natively in ES world as already explained in the previous post ... Moreover:
// getting rid for no reason // to an object that was already there // and configured as YOU expected! Person.prototype = { // now making `constructor` // an **enumerable** property constructor: Person // now adding more enumerable // properties ... // CONGRATS !!! // YOU NEED hasOwnProperty CHECKS NOW };As summary, one developer laziness "to rule them all" ... which explains why we should never actually care about
Object.defineProperties
performance if all we do in our code is duplicating objects, re-addressing prototypes, and making our own environment unreliable so that a loop without hasOwnProperty
check is considered unsafe from our own linter, the tool that supposes to tell us how to write a better code ... how badly screwed is all this?
Descriptors For All The Prototypes!
Instead of throwing away what the language we are using is gently providing to us, we can simply leave things as these are meant to be and enrich them with ease:function Person(){} Object.defineProperties( // enriching instead of replacing Person.prototype, { // everything we want // as Class.prototype } );If used for all classes, not only we can forget about
constructor
since, unless specified as descriptor, it will always be the expected one, which is the reason we keep reassigning it in the old ES3 pattern, we don't ask the GC to collect any default prototype object plus the constructor won't be enumerable!
Advantages About Descriptors
Passing through a clean operation asObject.defineProperties
is, is not just about preserving the original constructor
descriptor and its non-enumerability, it's rather exactly what we want in terms of reliable code, a topic that should never be under estimated in a JS world where many libraries and modules should cooperate together:
function Person(name) { this.age = 0; this.name = name; } Object.defineProperties( Person.prototype, { grownUp: { value: function () { return ++this.age; } } }); // it's a me! var me = new Person('andrea'); me.grownUp(); // 1 for(var key in me) { console.log(key, me[key]); } // age 1 // name andreaFirst of all, immutable methods are something actually we've been waiting and asking for and by default, using
Object.defineProperties
, we have such advantage!There's no way to reassign by accident
grownUp
to the me
variable, and this is, if you ask me, AWESOME!For the first time, the meaning of classical OOP makes sense in JavaScript, so that Classes could be defined somehow statically, it's still possible to enrich them later on, but all instances can trust the method they are using.
This has never been even closely similar in ECMAScript 3 era ... do you still think descriptors are such bad news?
Non Writable Properties
Going through all things that are mostly unknown, here the catch: by default all descriptors are not writable, not enumerable, and not configurable, which is actually a good news for Classes, but a not so good for instances and here is why:function Person(name){ this.name = name; } Object.defineProperties( Person.prototype, { // assuming we want a default // age of 0 per each born person age: { value: 0 }, // same method grownUp: { value: function () { return ++this.age; } } }); var me = new Person('andrea'); me.grownUp(); // 1 (due ++result) for(var key in me) { console.log(key, me[key]); } // age 0 <--- !!! // name andreaAs explained already in Part 1, setting a property to
writable:false
has a side effect very similar to the following one:
Object.defineProperty( Person.prototype, 'age', { get: function () { return 0; }, set: function () { // only under "use strict" it throws if (function(){return !this}()) { throw new Error('val is non writable'); } } });Above snippet means that any object, inheriting from
Person.prototype
, will find itself unable to directly define as instance.age = value
operation any value because "the invisible setter" will act like a guard, being invoked as we would expect any getter or setter defined in the prototype should!
Getters And Setters
As indeed is demonstrated in every old style benchmark we could find about ES3 prototypal inheritance, methods in the prototype are usually faster than methods defined at runtime.I am sure you've realized again that's a graph based on the worst case scenario ;-)
Anyway, getters and setters are methods, and as such, these are as slow as any other method inherited through an instance prototype will be ... so don't make a big deal out of them, JS engines are smart enough to speed up in a way methods would be speeded up:
function Rectangle(width, height) { this.width = width; this.height = height; } Object.defineProperties( Rectangle.prototype, { // returns the generic instance area area: { get: function () { return this.width * this.height; } } }); (new Rectangle(3, 3)).area; // 9 (new Rectangle(3, 2)).area; // 6Once we understand the power and the mechanism behind getters and setters, we can hardly complain about the behavior that
writable:false
introduced to classes, specially after reading this rationale!
Configurable, If Necessary!
Once again, the simple problem withwritable:false
is that once inherited, this is not ignored by the syntax:
var a = Object.defineProperty( {}, 'lol', {value: 'wut'} ); var b = Object.create(a); // same as ... function B(){} B.prototype = a; var b = new B; // so that ... b.lol = 'ahahahahahahahah'; // ... instead... b.lol; // 'wut'So, the bad news is that properties, if specified in a prototype as default values, should always be
writable:true
, but if these are meant to be specified as immutable, it's always possible to redefine them later on:
function One(){} Object.defineProperties( One.prototype, { valueOf: { value: function () { return 1; } } } ); var badAss = new One; Object.defineProperty( badAss, 'valueOf', { value: function () { return 2; } } ); 1 * badAss; // 2
I love this articles, finally I understand descriptors. I agree with everything you say except one thing:
ReplyDeleteI can't see how
Object.defineProperties(Person.prototype, {
talk: {
value: function() {
...
}
},
walk: {
value: function() {
...
}
},
jump: {
value: function() {
...
}
}
});
Is more readable than
_.extend(Person.prototype, {
talk: function() {
...
},
walk: function() {
...
},
jump: function() {
...
}
});
Actually I didn't use descriptors until now because they made my "classes" harder to read.
Also maybe you don't overwrite the default prototype on a base class
function Person() { }
Object.defineProperties(Person.prototype, { ... });
But you'll need to do it on a inherited class
function Employee() {
Person.call(this);
}
Employee.prototype = Object.create(Person.prototype, {
constructor: {
value: Employee
},
});
Anyway thanks for your article, I learn a lot with it :)
MatÃas glad you like it, but there's more coming plus I've never talked (yet) about readability.
ReplyDeleteHowever, one of redefine.js goal is to solve readability indeed so that if you look as examples or the How To you'll see it's more like writing coming ES6 Classes, with the ability to use descriptors anytime you want to.
About inheritance, you don't need to redefine the prototype neither.
Using Object.setPrototypeOf changes the link and it does not create a new object.
The same could be done via __proto__ but this is a different story that will be discussed in the next part.