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

Monday, February 02, 2015

A future friendly, backward compatible, class utility

I've roughly talked about es-class during #FEDLondon, but I haven't mentioned it here even once.

yet another class utility?

Not only there are tons of library and used since 10 years ago, but I've also gone far enough with many attempts.
I asked myself if this wouldn't have been "just another one", but the answer was instead: no, this one is going to work!
The main difference between this class utility and others, either from me or other authors, is that this time the design reference is the ECMAScript specification itself. es-class aim is to be as close as possible to the future of classes in JavaScript and it will be updated accordingly.

Why not just transpiling from ES6 then?

The amount of mobile browsers and embedded computing devices out there without the ability to source map and debug is still very high. We are OK if we develop for a Desktop browser that supports them, but everywhere else it could be very problematic.
Using an ES3 compatible syntax in production, writing ES5 syntax in development, and transpiling, if we'd like to, only a handy subset of the next version, aka ES6, is a win in terms of readability, debugging, and development.
Strawberry on top, there won't be any automagically decreased performance with sugar we don't control, and also there will be some special feature borrowed from the next version of ECMAScript, ES7 or however that will be called.

Tomorrow vs Today

As shown in the FED talk, here how we will write classes in JavaScript:
class TestB extends TestA {
  method() {
    /** do something ... */
  }
  get value() {
    /** return something ... */
  }
}
and this is how we can write them already via es-class
var TestB = Class({ extends: TestA,
  method() {
    /** do something ... */
  },
  get value() {
    /** return something ... */
  }
});
In order to make object short method definition available in ES3, we can use only a subset of 6to5:
6to5 --whitelist=es6.properties.shorthand TestB.js
Generating the following ES5 compatible syntax:
var TestB = Class({ extends: TestA,
  method: function method() {
    /** do something ... */
  },
  get value() {
    /** return something ... */
  }
});
If we'll drop the getter, syntax available since about 10 years but not implemented in IE8 or lower, we can have compatibility down to IE6 and all good old ES3 based engines. Don't worry about reserved keywords, these will be normalized by any minifier out there you are already familiar with.
// minified class TestB example
var TestB=Class({"extends":TestA,method:function(){}});
//               ^  see? ^ this it's automatic once minified

Lightweight Traits included

In order to compose classes and reuse modules as much as possible, we can take advantage of the with property, another previously reserved keyword that will be wrapped once minified for older browsers too. This is probably not the keyword that will be used in ES7 to attach traits to classes, but being historically a reserved one and being the most semantic one available (with: anotherObjectPropertiesAndMethods), I took it to make composition deadly simple:
// a trait is just a plain object
var UniqueID = {
  // with an optional init method
  init: function () {
    this._uniqueId = '\x00uniqueid:' +
                     Date.now() +
                     Math.random();
  },
  // and optional properties or methods
  get uid() {
    return this._uniqueId;
  }
};

var Book = Class({
  // one or more optional traits
  // will be initialized automatically
  with: [UniqueID],
  // and **before** the constructor
  constructor: function (title, author) {
    this.title = title;
    this.author = author;
  }
});

var js = new Book('blabla JS bla', 'Some Body');
js.uid; // a unique id
And of course there is the possibility to add more traits:
// another trait/mixin
var Storable = {
  init: function () {
    this._storableData = Object.create(null);
  },
  saveStorableAs: function (name, what) {
    this._storableData[name] = what;
  },
  toJSON: function () {
    return this._storableData;
  }
};

var Book = Class({
  with: [
    UniqueID,
    Storable
  ],
  constructor: function (title, author) {
    this.title = title;
    this.author = author;
  },
  save: function () {
    // save data
    this.saveStorableAs(this.title, {
      author: this.author,
      uid: this.uid
    });
    // and store it permanently
    localStorage.setItem('book', JSON.stringify(this));
  }
});

var js = new Book('blabla JS bla', 'Some Body');
js.save();

More goodness

If a semantic extends with a smart super and a with keyboard to bring in traits and mixins isn't enough, you might consider interesting the usage of grouped static properties:
var LightBulb = Class({
  // grouped statics, one place to find them all
  static: {
    ON: 'lightbulb_on',
    OFF:'lightbulb_off'
  },
  constructor: function () {
    this.lightState = LightBulb.OFF;
  },
  switch: function (how) {
    if (how !== LightBulb.OFF && how !== LightBulb.ON) {
      throw new Error('no quantum light allowed');
    }
    this.lightState = how;
  }
});

var kitchenBulb = new LightBulb();
kitchenBulb.switch(LightBulb.ON);
In case you are wondering, statics are inherited too.
var FlashBulb = Class({
  extends: LightBulb,
  switch: function (how) {
    // do whatever a lightbulb does
    this.super(how);
    // check that if it's on
    if (how === FlashBulb.ON && !this._flashBulbTimer) {
      // should go off in 50 ms
      this._flashBulbTimer = setTimeout(
        this.switch.bind(this, FlashBulb.OFF),
        50
      );
    } else {
      // clean up the timer once OFF again
      clearTimeout(this._flashBulbTimer);
      this._flashBulbTimer = null;
    }
  }
});

Interfaces too

This time inspired by TypeScript interfaces, es-class uses implements keyword to do, at definition time and only once, a check against possible interfaces expected to be found in the class itself.
// es-class interface example
var IGreetable = {
  /** Used to welcome someone */
  greet: function (message) {}
};

var Person = Class({
  // one or more interface to implement
  implements: IGreetable,
  greet: function (message) {
    console.log(message);
  }
});

If we try to remove the method from Person, we'll see a simple console warning, where available, like: "greet is not implemented"

As summary

In my 15+ years career I've been working with both classical and prototypal inheritance programming languages, actually avoiding in the past the simulated "classical to JS" approach as much as I could.
However, this classical pattern is now part of the fresh new baked specification and honestly, I think classical inheritance has been useful in large (huge!) code bases too, where classes ended up being the only clean or sane way to move forward.
Composing their behavior thought, is also something extremely handy and powerful and reusable, if done the right way.
With es-class we can code what we expect from the present and the future today, using eventually some partial transpiler to write faster and use handy features, without necessarily dropping performance on older browsers or needing full ES5 capabilities. Some little polyfill like es5-shim and dom4 or others, in order to enjoy without fearing missing source-maps and with a syntax ready to be refactored once our target engines will be ready.
Write today what you can use tomorrow in a fully backward compatible way, and have fun with es-class

3 comments:

  1. There are many serious problems with class inheritance, but it looks like you've been thoughtful about making composition easy. That's a step in the right direction.

    Still, seeing parent-child hierarchies throws up all kinds of red flags in my mind. See "The Two Pillars of JavaScript Pt 1: How to Escape the 7th Circle of Hell".

    ReplyDelete
  2. composition is a first class citizen here, actually I'm using es-class mostly composing functionalities.

    It's deadly simple and makes you forget about `super` and other class related things ;-)

    ReplyDelete

Note: Only a member of this blog may post a comment.