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

Friday, March 28, 2014

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

Once you've read, Part 4 is out ;-)
In Part 2, we have seen few advantages about using descriptors to define classes, let's see the example again:
// basic ES5 class definition
// the constructor
function Rectangle(width, height) {
  this.width = width;
  this.height = height;
}
Object.defineProperties(
  // enriching instead of replacing
  Rectangle.prototype, {
  // a generic method
  toString: {
    value: function () {
      return '[object Rectangle]';
    }
  },
  // a generic getter
  area: {
    get: function () {
      return this.width * this.height;
    }
  }
});

// quick example
var ret = new Rectangle(3, 2);
'' + ret; // [object Rectangle]
ret.area; // 6
So far so good, right? Aren't we missing something? Uh right ...

Extending Via Descriptors

In an ideal world, which is hopefully coming soon thanks to ES6, we could simply relink a generic prototype object instead of replacing it, something like:
// basic ES6 class extend
// the constructor
function Square(size) {
  Rectangle.call(this, size, size);
}
Object.defineProperties(
  // swap inheritance instead of
  // loosing the original prototype
  Object.setPrototypeOf(
    Square.prototype,
    Rectangle.prototype
  ), {
  // extra properties/overrides if needed
  toString: {
    value: function () {
      return '[object Square]';
    }
  }
});

// quick example
var sqr = new Square(3);
'' + sqr; // [object Square]
sqr.area; // 9
Unfortunately there is no Object.setPrototypeOf in ES5, while __proto__ does not appear even once in the specifications.
This is why 5.1 compatible engines like duktape, as example, will not work using the dirty __proto__ and cannot then hot-swap prototypes at runtime at all ... this was ES5.1

Extending the ES5 Way

In the previous post, Matías Quezada commented pointing out a couple of things, where the first one is about the need to reassign the prototype when it comes to extend.
However, what comes natural with ES5 is to redefine the original constructor as not enumerable, which is at least the closest behavior we would expect from a class.
// basic ES5 class extend
// the constructor
function Square(size) {
  Rectangle.call(this, size, size);
}
// the prototype redefined
Square.prototype = Object.create(
  Rectangle.prototype, {
  // but with the right constructor
  // and in a non enumerable way
  constructor: {
    value: Square
  },
  // extra properties/overrides if needed
  toString: {
    value: function () {
      return '[object Square]';
    }
  }
});
Understanding the difference between these patterns is essential, but as developers, we also would like to simplify the task and here I am.

A Basic ES5 Class Utility

The most simple and basic utility we could think of will probably look like the following one:
// simplifying the repeated pattern - v1
function Class(proto, descriptors) {'use strict';
  var extending = descriptors != null,
      d = extending ? descriptors : proto,
      constructor = (
        d.hasOwnProperty('constructor') ?
        d.constructor :
        d.constructor = {value: function Class(){}}
      ).value;
  return (extending ?
    (constructor.prototype = Object.create(
      typeof proto === 'function' ?
        proto.prototype : proto, d)) :
    Object.defineProperties(constructor.prototype, d)
  ).constructor;
}
Above code is quite compact and it provides the ability to rewrite our two example classes in this way:
var Rectangle = Class({
  constructor: {
    value: function (width, height) {
      this.width = width;
      this.height = height;
    }
  },
  toString: {
    value: function () {
      return '[object Rectangle]';
    }
  },
  area: {
    get: function () {
      return this.width * this.height;
    }
  }
});

// two arguments to extend
var Square = Class(
  Rectangle, {
  constructor: {
    value: function Square(size) {
      Rectangle.call(this, size, size);
    }
  },
  toString: {
    value: function () {
      return '[object Square]';
    }
  }
});
At least now we have a single way to define classes through descriptors ... what else could we do?

Descriptors VS Readability

The second point that Matías Quezada made in his comment was indeed about descriptor verbosity, which I believe goes back to the very first part of these posts:

@WebReflection @antirez property descriptors are terrible :( ...

— TJ Holowaychuk (@tjholowaychuk) March 20, 2014
If we believe these are good assumptions:
  1. we still want the ability to define getters and setters
  2. properties, if specified, are there to be marked as immutable, like constants, rather than defaults
  3. we want to use code that is less ambiguous as possible
  4. accordingly, properties with a shared default needs to be explicitly flagged as writable
  5. but function, for a class, will always be considered method of the class
Wondering what is this about? Let's see how we can make the code less verbose, still elegant, and never ambiguous:
var Rectangle = Class({
  constructor: function (width, height) {
    this.width = width;
    this.height = height;
  },
  toString: function () {
    return '[object Rectangle]';
  },
  area: {
    get: function () {
      return this.width * this.height;
    }
  }
});

var Square = Class(
  Rectangle, {
  constructor: function (size) {
    Rectangle.call(this, size, size);
  },
  toString: function () {
    return '[object Square]';
  }
});
In order to reach above improved state, the original Class required some make up:
// simplifying the repeated pattern - v2
var Class = (function(){'use strict';
  function descriptify(d, k) {
    return  typeof d[k] === 'function' &&
            (d[k] = {value: d[k]}), d;
  }
  function Class(proto, descriptors) {
    var d, extending = descriptors != null,
        d = Object.keys(
          d = extending ? descriptors : proto
        ).reduce(descriptify, d),
        constructor = (
          d.hasOwnProperty('constructor') ?
          d.constructor :
          d.constructor = {value: function Class(){}}
        ).value;
    return (extending ?
      (constructor.prototype = Object.create(
        typeof proto === 'function' ?
          proto.prototype : proto, d)) :
      Object.defineProperties(constructor.prototype, d)
    ).constructor;
  }
  return Class;
}());
We are still around 238 bytes minzipped so it's not a big deal and probably the simplest Class you can play around defining methods or, when it's necessary and understanding how, shared properties through descriptors.
If this is not enough, and we'd like to have a complete solution entirely based on ES5 and with extra patterns described in these posts too such the lazy reassignment, redefine.js is a good playground too, and you can see few examples and compare with above proposal which once again is very minimalistic, not so fancy in features, but surely fast, reliable and efficient, most likely all we need for our projects.

About Non Standard Behaviors

Warning
From now on, everything we'll explore will NOT be what we need to write on daily basis. Actually, most of the following alchemies are patterns that we'll never need in our life as coders.
However, being these post about telling you all things that books forgot to mention, I could not help myself going deeper in most dark and obscure details, proposing patters yu probably never cared about as the lazy assignment could be ... take the rest of this post as extra details, and keep thinking about what ES5 offers remembering this snippet

It has already been explained in Part 1 that writable properties cannot be directly redefined.
var Person = Class({
  name: { // as default
    value: 'anonymous'
  }
});

var me = new Person();
me.name = 'ag';
me.name; // anonymous
What I haven't told you yet, and this is still an active discussion/concern in es-discuss, is that in V8 and current node.js, constructors have super powers
var Person = Class({
  constructor: function (name) {
    // here it's writable anyway
    this.name = name;
  },
  name: { // as default
    value: 'anonymous'
  }
});

var me = new Person('ag');
me.name; // ag
Why we might think that's cool and ideal, not only this behavior is not adopted by other JS engines, is also kinda pointless since usually properties set in the constructor don't need a default, these will be replaced in there anyway so ... how about we just don't specify the default if we set it during initialization anyway?
var Person = Class({
  constructor: function (name) {
    this.name = name || 'anonymous';
  }
});

var me = new Person('ag');
me.name; // ag
If we really need a default, the better thing we can do is to specify the property as writable.
var Person = Class({
  constructor: function (name) {
    if (name) {
      this.name = name;
    }
  },
  name: {
    writable: true,
    value: 'anonymous'
  }
});

var me = new Person('ag');
me.name; // ag

The Dirty V8 Behavior

When I've talked about super powers, I didn't mention the super shenanigans too. As soon as something external "touches the freshly baked instance", the magical [[Set]] behavior breaks again.
var WTF1 = Class({
  constructor: function (tf) {
    this.what = tf;
  },
  what: {value:'nope'}
});

var WTF2 = Class({
  constructor: function (tf) {
    this.what = tf;
    // so far, so good, right ? ... now
    // this is a perfectly legit operation
    Object.getOwnPropertyDescriptor(this, 'what');
  },
  what: {value:'nope'}
});

var wtf1 = new WTF1('WTF'),
    wtf2 = new WTF2('WTF');

wtf1.what; // WTF
wtf2.what; // nope
I do hope that this madness will be standardized somehow ... I mean, even just passing the instance to Object will break as well ... anyway ...

Getters And Setters Bug

Forget V8 and most modern node.js, this time it's Androind 2.x and webOS time!
While the latter one is basically disappeared from the web scene, and I personally think it's a pity since my Pre 2 is almost fully ES5.1 spec compliant, better than any IE < 10, the first one is still widely used around the world, also quite cheap so usually a preferred choice in emerging markets.



As we can see in the dashboard, 20% or more is still a big amount of mobile Android platform users, and here the bug they'll be dealing with in ES5:
var hasConfigurableBug = !!function(O,d){
  try {
    O.create(O[d]({},d,{get:function(){
      O[d](this,d,{value:d})
    }}))[d];
  } catch(e) {
    return true;
  }
}(Object, 'defineProperty');
If hasConfigurableBug is false, it means that a lazily assigned property can be reconfigured without problems:
var Unconfigurable = Class({
  lazy: {
    get: function () {
      // here do amazing things
      // then reconfigure once the property
      // this.lazy = value; won't work
      // because we are inheriting get/set behavior
      // the only option is this one:
      Object.defineProperty(this, 'lazy', {value:
        // the assigned once property
        Math.random()
      });
      return this.lazy;
    }
  }
});

var rand = new Unconfigurable;

rand.lazy; // 0.5108735030516982
rand.lazy; // still same value:
           // 0.5108735030516982
However, if hasConfigurableBug is true, that operation will throw an error saying that is not possible to configure a property that has a getter or setter.
In few words, the bug is about defineProperty, for some reason unable to reconfigure what has been inherited, if this contains either a getter or a setter, or both.
Unfortunately, this bug has been a widely adopted in old mobile WebKit, so back to the initial feature detection, here is how we could obtain the same behavior in these browsers too:
var Unconfigurable = Class({
  lazy: {
    // needs eventually to be deleted later on
    configurable: hasConfigurableBug,
    get: function () {
      if (hasConfigurableBug) {
        var descriptor = Object.getOwnPropertyDescriptor(
          Unconfigurable.prototype, 'lazy');
        // remove it ...
        delete Unconfigurable.prototype.lazy;
      }
      // ... so that this won't fail
      Object.defineProperty(this, 'lazy',
        {value:Math.random()});
      if (hasConfigurableBug) {
        // "first time ever var makes sense" ^_^
        Object.defineProperty(
          Unconfigurable.prototype, 'lazy', descriptor);
      }
      return this.lazy;
    }
  }
});

var rand = new Unconfigurable;

rand.lazy; // 0.5108735030516982
rand.lazy; // still same value:
           // 0.5108735030516982
I know, this one is tough to digest, and that's why I've mentioned before redefine.js, however it's clear now how eventually solve the problem in a tiny feature detection that won't compromise performance.
There is still something we might want to do, partially to make latest snippet portable without accessing manually to a known prototype, partially to complete this 3rd post on descriptors ...

getPropertyDescriptor

Usually inheritance examples comes with 2 basic levels, but what if we inherited a behavior 3 or 4 levels up?
Here a very simple utility that will return undefined, or an ancestor descriptor with a object property that will point to the ancestor itself, property borrowed from Object.observe current proposal.
function getPropertyDescriptor(object, key) {
  do {
    // get the decriptor, if any
    var descriptor = Object.getOwnPropertyDescriptor(
      object, key);
    // otherwise if there is inheritance ... try again
  } while(!descriptor && (
    object = Object.getPrototypeOf(object)
  ));
  if (descriptor) {
    // set the target object
    descriptor.object = object;
  }
  return descriptor;
}
With this utility, the previous snippet of code would look slightly better:
var Unconfigurable = Class({
  lazy: {
    configurable: hasConfigurableBug,
    get: function () {
      if (hasConfigurableBug) {
        var descriptor = getPropertyDescriptor(
          this, 'lazy');
      }
      Object.defineProperty(this, 'lazy',
        {value:Math.random()});
      if (hasConfigurableBug) {
        Object.defineProperty(
          descriptor.object, 'lazy', descriptor);
      }
      return this.lazy;
    }
  }
});
To be brutally honest, in case we have multiple getters behavior redefined up the chain, above code won't solve much since there could be another descriptor up there ... oh well, I hope we'll never find ourself redefining lazy properties, instead of simple getters, more than once per inheritance chain.
In any case, for completeness, here the plural version of the function, where all descriptors are returned at once with all object references.
function getPropertyDescriptors(object) {
  var descriptors = {},
      has = descriptors.hasOwnProperty;
  function assign(name) {
    if (!has.call(descriptors, name)) {
      descriptors[name] =
        getPropertyDescriptor(object, name);
    }
  }
  do {
    (Object.getOwnPropertyNames ||
      Object.keys)(object).forEach(assign);
  } while(object = Object.getPrototypeOf(object));
  return descriptors;
}

Almost Completed

The Part 4 will come too and it will be about IE8 possibilities and some extra possibility we could have through descriptors. However, it was really essential to understand all potentials, pros and some weird cons, so that we can appreciate even more the next and final part of these posts.

2 comments:

PeterStJ said...

I have read all 3 parts and to tell you the truth it does look bad.

What you came up at the end of part 3 (the Class function) is really really not what I have been expecting from JS. Looks exactly like the hacks from MooTools time (dead now, thank God).

I know you are strongly opinionated and do not give s*** about other's opinions, but just looking at that code you have used as example gives headache to everyone I show it to, including JS developers, let alone devs with other languages.

I have been using JS for the last 5 years exclusively. However for the last 3 months I had to write PHP (bad choice as well) and Dart. I am not looking back. Ever. Which means something coming from a person who deals exclusively with it and is consulting JS for living...

Andrea Giammarchi said...

Peter I don't understand what is it that looks bad ... if you are talking about implementing the lazy assignment then a) you have no other way in ES6 to do that via syntax and b) it'a specific pattern not widely adopted that really should never be there if you don't need it.

The lazy assignment is not how you should write any getter or setter, is instead a very specify meant behavior that, if compatibility with old browsers is needed, needs some knowledge about how these behave.

What you should rather focus in here, is descriptors vs readability chapter classes examples, which is very clean and looks closer to what ES6 will come up with.

Last, but not least, if you want to write beautiful ES6 code like via ES5 and without needing to even think about these problems, redefine is just one options out of many.

I will underline that last snippet is absolutely not how we should write code though, it's very sad that's all you have left from these posts.