EDITING BOARD
RO
EN
×
▼ BROWSE ISSUES ▼
Issue 59

Front-end architecture - Common patterns of classical inheritance in JavaScript

Alexey Grinko
Front End Developer @ Crossover for Work



PROGRAMMING


There is a common understanding that front-end programming is less prone to architecture and design patterns, and more prone to the pursuit of more stable add-ons. Well, since this is an obvious shallow approach, we are going to use this article to throw a ray of light on classical inheritance in JavaScript, its common patterns, features and the frequent mistakes of its application. We'll consider examples of inheritance in Babel, Backbone JS and Ember JS, and we will try to derive the key principles of object-oriented inheritance for creating custom implementation with EcmaScript 5.

By using the word "classical", we refer to inheritance in OOP-style. It should be noted that there is no inheritance in pure JavaScript. Moreover, it lacks the notion of classes at all. Moreover, although the modern EcmaScript specification adds syntactic constructs for working with classes, this doesn't change the fact that it actually uses constructor functions and prototyping. Accordingly, this technique is often called "pseudo-classical" inheritance. It pursues, perhaps, the only goal - to represent code in the familiar OOP-style.

There are various techniques of inheritance, in addition to the classical one: functional, pure prototypal, fabric, using mixins. The very concept of inheritance, which gained high popularity among developers, is being criticized and contrasted with a reasonable alternative - composition. Thus, inheritance, especially in the classical approach, is not a panacea. Its expediency depends on the concrete situation in the concrete project. In this article, however, we are not going to delve into the question of advantages and disadvantages of this approach, but rather focus on the ways of its correct application.

Therefore, we decided to use OOP and classical inheritance in the language that does not support it originally. This solution is often adopted in large projects by developers accustomed to OOP in other languages. It is also used by many major frameworks: Backbone, Ember JS, etc., as well as the modern EcmaScript specification.

The best advice on using inheritance will be to use it as described in EcmaScript 6, with the following keywords: class, extends, constructor, and so on. If you have such an option, it is the best in terms of code readability and performance. All the following descriptions will be useful for the case of the old specification, when the project is already started with ES5 and transition to the new version does not seem available.

Evaluation criteria

Let's consider some popular examples of classical inheritance realization and analyze them under the following 5 criteria:

  1. Memory efficiency.

  2. Performance.

  3. Static properties and methods.

  4. Superclass reference.

  5. Cosmetic details.

First, obviously, make sure that the pattern is effective from the memory efficiency and performance point of view. In this respect, there are no special claims to the following examples from popular frameworks. However, in practice there often occur erroneous samples leading to memory leak and stack expansion, which will be discussed below.

The remaining listed criteria are related to code usability and readability. The implementations that are closer in syntax and functionality to the classical inheritance in other languages will be more "usable". For instance, reference to the superclass (the super keyword) is optional, but its presence is desirable for complete emulation of classical inheritance. By "cosmetic details" we mean the overall design of the code, the convenience of debugging, the use with the instanceof operator, and so on.

The «_inherits» function in Babel

Consider the inheritance in EcmaScript 6 and the output we get when compiling the code into ES5 with Babel. Below is an example of class extension in ES6.

class BasicClass {
    static staticMethod() {}
    constructor(x) {
        this.x = x;
    }

    someMethod() {}
}

class DerivedClass extends BasicClass {
    static staticMethod() {}
    constructor(x) {
        super(x);
    }
    someMethod() {
        super.someMethod();
    }
}

As one can see, the syntax is similar to other OOP languages, except for types and access modifiers. And this is the uniqueness of using ES6 with compiler: we can afford the convenient syntax and get the working ES5 code at the same time. None of the following examples can boast such syntactic simplicity, because the inheritance function there is ready-made, without syntax transformations.

The Babel compiler implements inheritance using a simple function called _inherits :

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" 
    && superClass !== null) {
      throw new TypeError("Super expression must" + 
        "either be null or a function, not " 
        + typeof superClass);
  }
  subClass.prototype = Object.create(superClass 
    && superClass.prototype, {
      constructor: {

         value: subClass,
         enumerable: false,
         writable: true,
         configurable: true
       }
  });
  if (superClass) Object.setPrototypeOf 
     ? Object.setPrototypeOf(subClass, superClass) 
     : subClass.__proto__ = superClass;
}

The key here is the following line:

subClass.prototype = Object.create(superClass.prototype);

This call creates an object with the specified prototype. The prototype property of subClass constructor refers to a new object whose prototype is superClass.prototype. Hence, this is a simple prototypal inheritance disguised as classical in the source code.

The inheritance of static fields is realized with the help of the following line:

Object.setPrototypeOf
  ? Object.setPrototypeOf(subClass, superClass)
  : subClass.__proto__ = superClass;

The constructor of the super class (i.e. function) is the prototype of the new class' constructor (i.e. of another function). This way, all static properties and methods of the super class become reachable from the subclass. In the absence of setPrototypeOf, Babel provides direct assignment of the prototype to the hidden property __proto__ - this method is not recommended, but it is suitable for the edge case of using old browsers.

The assignment of methods, both static and dynamic, occurs separately after calling _inherits by simply copying the references to the constructor or its prototype. When writing custom implementation of inheritance, one can use this example as a basis and add objects with dynamic and static fields to it as additional arguments.

The "super" keyword is replaced during compilation with the direct call of the prototype. E.g. super-constructor invocation from the example above is replaced by the following line:

return _possibleConstructorReturn(this
  , (DerivedClass.__proto__ 
    || Object.getPrototypeOf(DerivedClass))
    .call(this, x));

Babel uses many function-helpers that we are not going to cover here. The bottom line is that, along this line, the interpreter gets the prototype of the current class constructor, which is the constructor of the base class itself (see above), and calls it with the current context.

In a custom implementation with pure ES5, one can manually add a field _super to the constructor and its prototype in order to have a smart reference to the base class, for example:

function extend(subClass, superClass) {
    // ...
    subClass._super = superClass;
    subClass.prototype._super = superClass.prototype;
}

The «extend» function in Backbone JS

Backbone JS provides the extend function for extending the library's classes: Model, View, Collection, etc. If desired, it may be borrowed for one's own purposes. Below is the code for the extend function from Backbone version 1.3.3.

var extend = function(protoProps, staticProps) {
  var parent = this;
  var child;
  // The constructor function for the new subclass 
  // is either defined by you
  // (the "constructor" property in your `extend` 
  // definition), or defaulted
  // by us to simply call the parent constructor.

  if (protoProps && _.has(protoProps,'constructor')){
    child = protoProps.constructor;
  } else {
    child = function(){ return parent.apply(this,
     arguments); };
  }

  // Add static properties to the constructor 
  // function, if supplied.

  _.extend(child, parent, staticProps);
  // Set the prototype chain to inherit from 
  // `parent`, without calling
  // `parent`'s constructor function and add 
  // the prototype properties.

  child.prototype = _.create(parent.prototype, 
    protoProps);

  child.prototype.constructor = child;
  // Set a convenience property in case the parent's  
  // prototype is needed later.

  child.__super__ = parent.prototype;

  return child;
}
;

An example of its usage looks like the following:

var MyModel = Backbone.Model.extend({
  constructor: function() {
  // your constructor; its usage is optional,
  // but when used it requires the super-constructor 
  // to be called:

    Backbone.Model.apply(this, arguments);
    },
    toJSON: function() {

  // the method is overridden, but the original 
  // method can be called via "__super_"

   MyModel.__super__.toJSON.apply(this, arguments);
  }
}, {
    staticMethod: function() {}
});

This function implements the extension of the base class supporting its own constructor and static fields. It returns a constructor function of the class. The actual inheritance is done using the following line similar to the example from Babel:

child.prototype = _.create(parent.prototype, 
  protoProps);

The _.create() function is similar to Object.create() from ES6, but it is implemented by Underscore JS lib. Its second argument allows for the writing of protoProps properties and methods, received when calling extend, right off to the prototype.

The inheritance of the static fields is implemented by simply copying references (or values) from the super class and from the object with static fields received as a second argument of the extend function:

_.extend(child, parent, staticProps);

Specifying the constructor is optional. It is done inside of the class declaration under the form of the "constructor" method. When using the constructor, it is required to call the parent class constructor (just like in other languages), so developers usually use the initialize method instead. It is called from the base constructor.

The keyword __super__ is only a convenient addition, because the call to the super method still occurs by the name of the particular method and by passing this context. Without this, such a call would lead to a loop in the case of a multi-level chain of inheritance. The method of the superclass, whose name is usually known in the current context, can be called directly, so this keyword is just a shortcut:

Backbone.Model.prototype.toJSON.apply(this,arguments);

In terms of code, the extension of classes in Backbone is pretty neat. You do not have to manually create a class constructor and associate it with the parent class on the different line of code. This convenience has its price - debugging difficulties. In the browser debugger, all instances of the classes inherited this way have the same constructor name declared within the extend function - "child". This disadvantage may seem insignificant until you encounter it in practice when debugging a chain of classes. It becomes difficult to understand which class the given object instantiates and which class it derives from. Here is an example from Google Chrome's console:

When using inheritance from Babel, this chain seems more convenient:

Another drawback is that the constructor property is enumerable, i.e. listed when traversing an instance of a class in a "for-in" loop. It is irrelevant, but Babel also took care of this, declaring the constructor with the list of necessary modifiersЕщё одним недостатком является то, что свойство constructor является enumerable, т.е. перечисляемым при обходе экземпляра класса в цикле «for-in». It makes no odds, but Babel cared about it as well by listing all needed modifiers when declaring the constructor.

The superclass reference in Ember JS

Ember JS uses inherits function implemented by Babel as well as its own implementation of extend - very complicated and heaped up, supporting mixins etc. There is simply not enough space in this article to bring its code here. And this very fact already calls into question the performance of this implementation when using it for one's very own purposes outside the framework.

Of special interest is the implementation of the "super" keyword in Ember. It allows to call a super method without specifying the concrete name of the method, for instance:

var MyClass = MySuperClass.extend({
    myMethod: function (x) {
        this._super(x);
    }
});

How does this work? How does Ember know which method to call when appealing to the versatile _super method without transforming the code? The answer lies in the complex processing of classes and a smart function _wrap whose code is given next:

function _wrap(func, superFunc) {
  function superWrapper() {
    var orig = this._super;
    this._super = superFunc; // the magic is here
    var ret = func.apply(this, arguments);
    this._super = orig;
    return ret;
  }
  // some irrelevant piece of code is omitted here

  return superWrapper;
}

When deriving a class, Ember goes through all of its methods and invokes this wrapper for each one. It replaces each original function with superWrapper. Pay attention to the line marked with a comment. The _super property now contains a reference to the parent method corresponding to the name of the method being called (all work to determine the correspondences had occurred during the creation of the class when calling extend). On the next line, the original function is called, from inside of which one can appeal to the _super as to the parent method. The _super property is then reset back to its original value. This allows to use it in deep chain calls.

The idea is undoubtedly interesting, and it can be applied in one's custom implementation of inheritance. However, there is an important caveat: the complexity of this function impairs its performance. Each class method (at least among those having parent methods with the same name), whether the _super is used there or not, get wrapped into a separate function. When a deep chain of the same class' methods calls it, stack expansion follows. This is especially crucial for methods that are called regularly, in a loop, or when rendering the UI. Therefore, we can say that this implementation is too cumbersome and does not justify the advantage in the form of an abridged notation.

The most common mistake

In practice, one of the most common and dangerous mistakes is creating an instance of the parent class while extending it. Here is an example of the code which should always be avoided:

function BasicClass() {
    this.x = this.initializeX();
    this.runSomeBulkyCode();
}
// ...declaration of BasicClass methods 
// in prototype...

function SubClass() {
    BasicClass.apply(this, arguments);
    this.y = this.initializeY();
}

// the inheritance
SubClass.prototype = new BasicClass();
SubClass.prototype.constructor = SubClass;

// ...declaration of SubClass methods in prototype...

new SubClass(); // instantiating

This code would work. It will let SubClass derive properties and methods of the parent class. However, while associating the classes via prototype, there is an instance of the parent class created and its constructor is invoked. It leads to extra actions, especially if the constructor performs much work while initializing an object (runSomeBulkyCode). This might also lead to some hard-to-detect errors when properties initialized in the parent constructor (this.x) are written to the prototype of all instances of SubClass instead of the instances themselves. Furthermore, the same BasicClass constructor is called then, again, from the subclass's constructor.

If the parent constructor requires some parameters when being called, this mistake is hard to make, but otherwise it's quite likely to occur.

Instead, an empty object, whose prototype is the prototype property of the parent class, should be created each time:

SubClass.prototype = Object.create(
  BasicClass.prototype);

Summary

We have given some examples of the pseudo-classical inheritance implementation in the Babel compiler (ES6-to-ES5) and in frameworks such as Backbone JS and Ember JS. Below is a comparative table of all three implementations structured by the criteria described earlier. The performance was evaluated not in absolute units, but in values relative to each other, based on the number of operations and cycles in each sample. In general, the difference in performance is not significant, because inheritance is performed once at the initial stage of the application and is not repeated again.

All the examples above have their pros and cons, but the most practical one can be considered the implementation used by Babel. As mentioned above, if possible, one should use the inheritance specified in EcmaScript 6 with compilation into ES5. In the absence of such an opportunity, it is recommended to write a custom implementation of the extend function based on the sample from the Babel compiler, taking into account the above remarks and features from other examples. Thus, inheritance can be implemented in the most flexible and suitable way for one's specific project.

- usage of Babel is perfect when combined with ES6; when writing a custom implementation for ES5 based on it, static fields and superclass reference should be implemented manually.

References

  1. JavaScript.ru

  2. David Shariff. JavaScript Inheritance Patterns

  3. Eric Elliott. 3 Different Kinds of Prototypal Inheritance: ES6+ Edition.

  4. Wikipedia - Composition over inheritance

  5. Mozilla Developer Network

  6. Babel (source code)

  7. Backbone JS (source code)

  8. Ember JS (source code)

Conference TSM

VIDEO: ISSUE 109 LAUNCH EVENT

Sponsors

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects

VIDEO: ISSUE 109 LAUNCH EVENT