My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Wednesday, March 26, 2014

What Books Didn't Tell You About ES5 Descriptors - Part 2

Once you've read, Part 3 is out ;-)
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, using Object.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 for Object.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 as Object.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 andrea
First 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 andrea
As 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; // 6
Once 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 with writable: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

Still Missing

I believe in this post there's a lot of extra material to test against and think about ... but I assure you, the road to mastering ES5 descriptors is still ahead, and a long one, so please come back in few days to see what else you probably didn't know about the current environment your JavaScript is running against ;-)

2 comments:

Unknown said...

I love this articles, finally I understand descriptors. I agree with everything you say except one thing:

I 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 :)

Andrea Giammarchi said...

Matías glad you like it, but there's more coming plus I've never talked (yet) about readability.

However, 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.