The History of Simulated Classes in JavaScript

It’s one of the most controversial aspects of the language, yet something that officially landed in 2015 specifications.

JavaScript and classes have been simulated for years and even if there is now a proper syntax to define them, the nature of the langauge will inevitably fallback to its prototypal inheritance.

This is a walk through the past, the present and the possible future of JS classes.

The Good Old ES3 Way

It’s still the universally compatible way to simulate a class. It works with every JS engine from IE5 (probably even IE4) and yet it works with most updated environments: it’s the classic function definition with its prototype pollution.

function Rectangle(width, height) {
  this.width = width;
  this.height = height;
}

Rectangle.prototype.area = function () {
  return this.width * this.height;
};

This clear and straight forward way to simulate classes has never been that welcomed though, and the community tried for years to ignore prototypal inheritance and preferred pretty much every attempt to hide that somehow, and behind the name class.

Frameworks and libraries such MooTools, Prototype, or people like John Resig, historically provided some sort of classical inheritance abstraction, something that at the end of the day was making classes defined through a more convenient object literal, something we could summarized as utility more or less as in the following code:

function ES3Class(obj) {
  var
    // if there isn't a constructor, create one
    constructor = obj.hasOwnProperty('constructor') ?
      obj.constructor : function () {},
    key
  ;
  for (key in obj) {
    // per each own property in the received object
    if (obj.hasOwnProperty(key) && key !== 'constructor') {
      // copy such property to the constructor prototype
      constructor.prototype[key] = obj[key];
    }
  }
  // return what will be used to create new Instances
  return constructor;
}

Ignoring for/in possible quirks regarding non enumerable bug in all IE browsers less than version 9, above code is basically all we need to define in a more “classy“ way our program in every ECMAScript 3 compatible engine.

var Rectangle = ES3Class({
  constructor: function (width, height) {
    this.width = width;
    this.height = height;
  },
  area: function () {
    return this.width * this.height;
  }
});

var r = new Rectangle(3, 7);
r.area(); // 21

Extending in ES3

The prototypal inheritance is some sort of “invisible chain” between objects. There is a link between them, and this link is created at runtime whenever the new keyword is used.

var instance = new function Class() {};
//     ↑                              .prototype
//     └──────────────────────────────┘
//     "invisibly" linked to the Class.prototype

instance.constructor; // will be the function Class

// proof that Class.prototype and the instance are related
instance.constructor.prototype.isPrototypeOf(instance);
// true

There’s surely way more to say about this “invisible link”, but what’s important to remember is that this is absolutely unique per each object. Such restriction is actually very similar to the one found in classical inheritance: a class can extend only one other class.

This makes it easy to simulate such behavior even in good old ES3 engines, we just need to create a temporary invisible link for the prototype we’d like to extend, so that every new instance will be linked to such object.

var ES3Extends = (function () {

  // intermediate function
  // used to invisibly link a new instance
  // to the currently assigned prototype
  function Bridge(constructor) {
    // assign the right constructor
    // instead of using the inherited one
    this.constructor = constructor;
    // clean up the link as soon as executed so that
    // no object will be kept in memory until the next call
    Bridge.prototype = Function.prototype;
  }

  // the utility to extend another "class"
  return function ES3Extends(parent, obj) {
    // get the constructor or create a new one
    var constructor = obj.hasOwnProperty('constructor') ?
        obj.constructor : (obj.constructor = function () {});
    // setup what will be the invisible link
    Bridge.prototype = parent.prototype;
    // reassign the constructor prototype using
    // an instance linked to parent.prototype
    // ensuring the right constructor
    constructor.prototype = new Bridge(constructor);
    // use previous utility that will copy
    // all properties over the constructor.prototype
    return ES3Class(obj);
  };

}());

Now that we have the ability to extend, we can verify that everything works fine defining a new Class.

// requires still the ES3Class function
// and the previously defined Rectangle class
var Square = ES3Extends(Rectangle, {
  constructor: function (size) {
    Rectangle.call(this, size, size);
  }
});

var s = new Square(3);
s.area(); // 9

// how to verify the inheritance is preserved?
s instanceof Square;    // true
s instanceof Rectangle; // true

Great, problem solved for all classical inheritance lovers: hurrah!!!

The ES5 Way

The main difference between ES5 and ES3 is the ability to define properties through descriptors, together with the ability to create invisible links through Object.create.

This gives developers the power to define properties similar to the way native constructors like Object or Array have: non enumerable. No more worries about for/in loops, no more undesired properties on our way.

The raw version of these new features is shown here:

function Rectangle(width, height) {
  this.width = width;
  this.height = height;
}

Object.defineProperties(
  Rectangle.prototype,
  {
    // defined as if it was a native method
    area: {
      enumerable: false,
      configurable:  true,
      writable: true,
      value: function area() {
        return this.width * this.height;
      }
    }
  }
);

In ES5 there are two kind of descriptors: data descriptors and accessors. While methods can always be defined as data descriptor, accessors give us the ability to implement nicer semantics through getters and setters, so that instead of r.area() we can simply ask for r.area.

Object.defineProperties(
  Rectangle.prototype,
  {
    // defined as if it was a native method
    area: {
      enumerable: false,
      configurable:  true,
      get: function area() {
        return this.width * this.height;
      }
    }
  }
);

As we can see, if there is one thing ES5 was very good at, is its verbosity. This is why most developers ignored actually all new possibilities through classes and kept using old libraries and old patterns without bothering much: these developers are still using for/in loops with a mandatory check for obj.hasOwnProperty, sometimes forgetting that in ES5 there are also objects that do not even inherit the hasOwnProperty method and could fail all over the place.

Since nobody likes to type all that code to define a method or a getter, let’s create an ES5 utility that generates classes for us.

function ES5Class(obj) {
  // if there isn't a constructor, create one
  var constructor = obj.hasOwnProperty('constructor') ?
        obj.constructor : function () {};

  // per each own property, enumerable or not
  Object.getOwnPropertyNames(obj).forEach(function (key) {
    // grab the descriptor
    var descriptor = Object.getOwnPropertyDescriptor(obj, key);
    // force it as non enumerable
    descriptor.enumerable = false;
    // set it directly in the constructor.prototype
    Object.defineProperty(constructor.prototype, key, descriptor);
  });

  // return what will be used to create new Instances
  return constructor;
}

We can now improve our classes definition in a more handy and clean way, using the ability to also define getters and setters directly through the object literal representation which is also part of the standard.

var Rectangle = ES5Class({
  constructor: function (width, height) {
    this.width = width;
    this.height = height;
  },
  get area() {
    return this.width * this.height;
  }
});

// just for testing purpose
var r = new Rectangle(5, 3);
r.area; // 15

That is way nicer than before, isn’t it?

Extending in ES5

As previously mentioned, there is an easier way to create prototypal inheritance in ES5 thanks to Object.create, so that we won’t need to have an intermediate, and quite ugly, Bridge function which only aim is to create such link.

Following the raw version of how to extend classes in ES5:

function Square(size) {
  // invoke the super constructor
  Rectangle.call(this, size, size);
}

// create inheritance and define extra properties
Square.prototype = Object.create(
  // the invisible linked object
  Rectangle.prototype,
  // properties to write on top
  {
    // we want to have Square as own constructor
    // and not the inherited Rectangle one
    constructor: {
      enumerable: false,
      configurable:  true,
      writable: true,
      value: Square
    }
    // optionally define here more methods/accessors
  }
);

We are unfortunately back to verbosity-land and lost readability on our way; if we think how pointless would be to read the same kind of descriptor over and over per each method of a generic class, we realize having an utility like the one used before in ES3 is a must.

function ES5Extends(parent, obj) {
  // grab the constructor or assign one
  var constructor = obj.hasOwnProperty('constructor') ?
      obj.constructor : (obj.constructor = function () {});

  // link its prototype to the parent one
  constructor.prototype = Object.create(parent.prototype);

  // use the previous utility that will copy
  // all properties over the constructor.prototype
  // as non enumerable
  return ES5Class(obj);
}

That is pretty much it. We can still use the power of object literal to define and extend any class we want.

var Square = ES5Extends(Rectangle, {
  constructor: function (size) {
    // invoke the super constructor
    Rectangle.call(this, size, size);
  }
});

// for testing purpose
var s = new Square(5);
s.area; // 25

Nice and simple, we have a proper and easy way to define classes in ES5 too!

ES6 Classes

The not widely supported (yet) but most modern standard way to define classes, is through the official syntax proposed in latest specifications. Most of the concepts previously described, are basically what happens behind the scene once we write classes.

However, there are at least 3 major improvements over what we’ve seen so far:

class Rectangle {

  // method definition shortcut
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  // note: NO comma needed
  get area() {
    return this.width * this.height;
  }

  // public static getter
  // reachable through
  // Rectangle.SHAPE
  static get SHAPE() {
    return 'Rectangle';
  }

}


// extending
class Square extends Rectangle {
  constructor(size) {
    // super reference
    super(size, size);
  }

  // Square.SHAPE
  static get SHAPE() {
    return 'Special ' + super.SHAPE;
  }

}

Differently from previous ECMAScript specifications, there are also other rules to follow:

In order to be able to define a property we can still use the good old Object.defineProperty(Rectangle, 'SHAPE', {value: 'Rectangle'}) but we don’t have any shortcut to do that directly at class definition time and same goes for properties, whenever it makes sense having them on a prototype (default values or edge-cases scenario).

But most important of all, at the end of the day everything said already about prototypal inheritance is still the same and ES6 classes are nothing but some sugar on top of what we could have done already via ES5, plus a useful simplification through the super keyword.

The Future

Classes are out, but these are not finalized. There are features developers keep asking, like lightweight traits/mixins, possibly types guards, maybe the ability to define constants directly at definition time and more such methods, or prototype, decorators.

Classes are also most of the time transpiled into ES5 via tools such Babel or others, in order to grant compatibility with older browsers, but not always an option in more constrained environments such Espruino, Duktape or others.

This is still an era where some utility might become handy, using what’s simple to fix via transpilers without compromising the logic, also a way to experiment new possibilities such traits, statics, or even interfaces, and all this is what es-class, among other similar projects, does: something that goes close to ES6, offering more features and way wider compatibility.

var Rectangle = Class({
  static: {
    SHAPE: 'Rectangle'
  },
  constructor(width, height) {
    this.width = width;
    this.height = height;
  },
  get area() {
    return this.width * this.height;
  }
});

var Square = Class({ extends: Rectangle,
  static: {
    SHAPE: 'Special ' + Rectangle.SHAPE
  },
  constructor(size) {
    this.super(size, size);
  }
});

Using something like babel --presets es2015 above-example.js will automatically make object methods shorthands compatible with every ES5 engine and every minifier will eventually make reserved keywords such extends or static compatible with older ES3 engine, es-class will take care of the rest and fallback with old de-facto standards behavior for getters and setters too, in case it’s necessary, so that we can have a future compatible, or at least extremely easy to migrate, backward compatible, down to very old browsers, simple and elegant class definition.

Light interfaces and mixins are included, give it a try if you’d like to play around and compose inheritance instead of simply extending only one class per time ;-)

The ES4 Way

Classes, and also types, have been in the air for very long time. Netscape proposed them already in 2002 and while these never made it as defined in those days in JavaScript, these have been implemented first time in ActionScript 2.0, the programming language that lasted 1 year and half before version 3.0 went out, and version 1.0 projects faded away.

These days there is another project that looks similar to AS 2.0, but would like to better follow current ES6 specifications: TypeScript, which brings in more features than those available these days, more similar to what my es-class project does, beside TypeScript needing its own transpiler to work as expected.

Why Classes at all ?

I’ve personally never proposed classes in any of the team I’ve worked with during the last 16 years. However, almost every team wanted, had already, or created, a class based system/application, and I’ve also implemented most of what I’ve shown in this post and used it in production.

Somebody is very religious about how bad or awesome classes are, all I can say from my experience is that classes make it easy to describe intents, to separate concerns through namespaces, and to setup any sort of object through their constructor.

This is convenient, simple to explain, simple to talk about, simple to use and define. I like when teams don’t have to argue every day if instanceof should be banned forever from the known universe and they actually care about shipping awesome stuff instead because “who actually cares“ if that wasn’t the problem!

Accordingly, use classe whenever you think these make your life easier to organize and understand, stick with whatever else you prefer that solves your problems. Classes might be just fine, specially as entry point to compose prototypes via mixins you can easily reuse.

Mixins, for example, are indeed an awesome pattern for small to big projects, and classes compatible with mixins just make everything easier to compose, test, and recycle so, again, if I can give you an advice: just do what you think works best for your daily job!

P.S. About Used Examples

Caveats and quirks behind classes simulations are many. I do not recommend to use simplified utilities I’ve shown in this very specific posts, these have been created just to explain more or less what happens behind the scene.

Please use a mature, battle tested, class solution, like es-class, Babel, or TypeScript could be, and avoid creating yet a new one: there are I think thousands attempts already in npm I’m sure the world doesn’t need another one ;-)

Andrea Giammarchi

Fullstack Web Developer, Senior Software Engineer, Team Leader, Architect, Node.js, JavaScript, HTML5, IoT, Trainer, Publisher, Technical Editor