Patterns for Object Inheritance in JavaScript ES2015

Tim Severien
Share

A child reading in bed with a torch, with scary silhouettes cast on the wall by toys

With the long-awaited arrival of ES2015 (formerly known as ES6), JavaScript is equipped with syntax specifically to define classes. In this article, I’m going to explore if we can leverage the class syntax to compose classes out of smaller parts.

Keeping the hierarchy depth to a minimum is important to keep your code clean. Being smart about how you split up classes helps. For a large codebase, one option is to create classes out of smaller parts; composing classes. It’s also a common strategy to avoid duplicate code.

Imagine we’re building a game where the player lives in a world of animals. Some are friends, others are hostile (a dog person like myself might say all cats are hostile creatures). We could create a class HostileAnimal, which extends Animal, to serve as a base class for Cat. At some point, we decide to add robots designed to harm humans. The first thing we do is create the Robot class. We now have two classes that have similar properties. Both HostileAnimal and Robot are able to attack(), for instance.

If we could somehow define hostility in a separate class or object, say Hostile, we could reuse that for both Cat as Robot. We can do that in various ways.

Multiple inheritance is a feature some classical OOP languages support. As the name suggests, it gives us the ability create a class that inherits from multiple base classes. See how the Cat class extends multiple base classes in the following Python code:

class Animal(object):
  def walk(self):
    # ...

class Hostile(object):
  def attack(self, target):
    # ...

class Dog(Animal):
  # ...

class Cat(Animal, Hostile):
  # ...

dave = Cat();
dave.walk();
dave.attack(target);

An Interface is a common feature in (typed) classical OOP languages. It allows us to define what methods (and sometimes properties) a class should contain. If that class doesn’t, the compiler will raise an error. The following TypeScript code would raise an error if Cat didn’t have the attack() or walk() methods:

interface Hostile {
  attack();
}

class Animal {
  walk();
}

class Dog extends Animal {
  // ...
}

class Cat extends Animal implements Hostile {
  attack() {
    // ...
  }
}

Multiple inheritance suffers from the diamond problem (where two parent classes define the same method). Some languages dodge this problem by implementing other strategies, like mixins. Mixins are tiny classes that only contain methods. Instead of extending these classes, mixins are included in another class. In PHP, for example, mixins are implemented using Traits.

class Animal {
  // ...
}

trait Hostile {
  // ...
}

class Dog extends Animal {
  // ...
}

class Cat extends Animal {
  use Hostile;
  // ...
}

class Robot {
  use Hostile;
  // ...
}

A Recap: ES2015 Class Syntax

If you haven’t had the chance to dive into ES2015 classes or feel you don’t know enough about them, be sure to read Jeff Mott’s Object-Oriented JavaScript — A Deep Dive into ES6 Classes before you continue.

In a nutshell:

  • class Foo { ... } describes a class named Foo
  • class Foo extends Bar { ... } describes a class, Foo, that extends an other class, Bar

Within the class block, we can define properties of that class. For this article, we only need to understand constructors and methods:

  • constructor() { ... } is a reserved function which is executed upon creation (new Foo())
  • foo() { ... } creates a method named foo

The class syntax is mostly syntactic sugar over JavaScript’s prototype model. Instead of creating a class, it creates a function constructor:

class Foo {}
console.log(typeof Foo); // "function"

The takeaway here is that JavaScript isn’t a class-based, OOP language. One might even argue the syntax is deceptive, giving the impression that it is.

Composing ES2015 Classes

Interfaces can be mimicked by creating a dummy method that throws an error. Once inherited, the function must be overridden to avoid the error:

class IAnimal {
  walk() {
    throw new Error('Not implemented');
  }
}

class Dog extends IAnimal {
  // ...
}

const robbie = new Dog();
robbie.walk(); // Throws an error

As suggested before, this approach relies on inheritance. To inherit multiple classes, we will either need multiple inheritance or mixins.

Another approach would be to write a utility function that validates a class after it was defined. An example of this can be found in Wait A Moment, JavaScript Does Support Multiple Inheritance! by Andrea Giammarchi. See section “A Basic Object.implement Function Check.”

Time to explore various ways to apply multiple inheritance and mixins. All examined strategies below are available on GitHub.

Object.assign(ChildClass.prototype, Mixin...)

Pre-ES2015, we used prototypes for inheritance. All functions have a prototype property. When creating an instance using new MyFunction(), prototype is copied to a property in the instance. When you try to access a property that isn’t in the instance, the JavaScript engine will try to look it up in the prototype object.

To demonstrate, have a look at the following code:

function MyFunction () {
  this.myOwnProperty = 1;
}
MyFunction.prototype.myProtoProperty = 2;

const myInstance = new MyFunction();

// logs "1"
console.log(myInstance.myOwnProperty);
// logs "2"
console.log(myInstance.myProtoProperty);

// logs "true", because "myOwnProperty" is a property of "myInstance"
console.log(myInstance.hasOwnProperty('myOwnProperty'));
// logs "false", because "myProtoProperty" isn’t a property of "myInstance", but "myInstance.__proto__"
console.log(myInstance.hasOwnProperty('myProtoProperty'));

These prototype objects can be created and modified at runtime. Initially, I tried to use classes for Animal and Hostile:

class Animal {
  walk() {
    // ...
  }
}

class Dog {
  // ...
}

Object.assign(Dog.prototype, Animal.prototype);

The above doesn’t work because class methods are not enumerable. Practically, this means Object.assign(...) doesn’t copy methods from classes. This also makes it difficult to create a function that copies methods from one class to another. We can, however, copy each method manually:

Object.assign(Cat.prototype, {
  attack: Hostile.prototype.attack,
  walk: Animal.prototype.walk,
});

Another way is to ditch classes and use objects as mixins. A positive side-effect is that mixin objects cannot be used to create instances, preventing misuse.

const Animal = {
  walk() {
    // ...
  },
};

const Hostile = {
  attack(target) {
    // ...
  },
};

class Cat {
  // ...
}

Object.assign(Cat.prototype, Animal, Hostile);

Pros

  • Mixins cannot be initialized

Cons

  • Requires an extra line of code
  • Object.assign() is a little obscure
  • Reinventing prototypical inheritance to work with ES2015 classes

Composing Objects in Constructors

With ES2015 classes, you can override the instance by returning an object in the constructor:

class Answer {
  constructor(question) {
    return {
      answer: 42,
    };
  }
}

// { answer: 42 }
new Answer("Life, the universe, and everything");

We can leverage that feature to compose an object from multiple classes inside a subclass. Note that Object.assign(...) still doesn’t work well with mixin classes, so I used objects here as well:

const Animal = {
  walk() {
    // ...
  },
};

const Hostile = {
  attack(target) {
    // ...
  },
};

class Cat {
  constructor() {
    // Cat-specific properties and methods go here
    // ...

    return Object.assign(
      {},
      Animal,
      Hostile,
      this
    );
  }
}

Since this refers to a class (with non-enumerable methods) in above context, Object.assign(..., this) doesn’t copy the methods of Cat. Instead, you will have to set fields and methods on this explicitly in order for Object.assign() to be able to apply those, like so:

class Cat {
  constructor() {
    this.purr = () => {
      // ...
    };

    return Object.assign(
      {},
      Animal,
      Hostile,
      this
    );
  }
}

This approach is not practical. Because you’re returning a new object instead of an instance, it is essentially equivalent to:

const createCat = () => Object.assign({}, Animal, Hostile, {
  purr() {
    // ...
  }
});

const thunder = createCat();
thunder.walk();
thunder.attack();

I think we can agree the latter is more readable.

Pros

  • It works, I guess?

Cons

  • Very obscure
  • Zero benefit from ES2015 class syntax
  • Misuse of ES2015 classes

Class Factory Function

This approach leverages JavaScript’s ability to define a class at runtime.

First, we will need base classes. In our example, Animal and Robot serve as base classes. If you want to start from scratch, an empty class works, too.

class Animal {
  // ...
}

class Robot {
  // ...
}

Next, we have to create a factory function that returns a new class that extends class Base, which is passed as a parameter. These are the mixins:

const Hostile = (Base) => class Hostile extends Base {
  // ...
};

Now we can pass any class to the Hostile function which will return a new class combining Hostile and whatever class we passed to the function:

class Dog extends Animal {
  // ...
}

class Cat extends Hostile(Animal) {
  // ...
}

class HostileRobot extends Hostile(Robot) {
  // ...
}

We could pipe through several classes to apply multiple mixins:

class Cat extends Demonic(Hostile(Mammal(Animal))) {
  // ...
}

You can also use Object as a base class:

class Robot extends Hostile(Object) {
  // ...
}

Pros

  • Easier to understand, because all information is in the class declaration header

Cons

  • Creating classes at runtime might impact startup performance and/or memory usage

Conclusion

When I decided to research this topic and write an article about it, I expected JavaScript’s prototypical model to be helpful for generating classes. Because the class syntax makes methods non-enumerable, object manipulation becomes much harder, almost impractical.

The class syntax might create the illusion that JavaScript is a class-based OOP language, but it isn’t. With most approaches, you will have to modify an object’s prototype to mimic multiple inheritance. The last approach, using class factory functions, is an acceptable strategy for using mixins to compose classes.

If you find prototype-based programming restrictive, you might want to look at your mindset. Prototypes provide unparalleled flexibility that you can take advantage of.

If, for any reason, you still prefer classical programming, you might want to look into languages that compile to JavaScript. TypeScript, for example, is a superset of JavaScript that adds (optional) static typing and patterns you will recognize from other classical OOP languages.

Are you going to use either of above approaches in your projects? Did you find better approaches? Let me know in the comments!

This article was peer reviewed by Jeff Mott, Scott Molinari, Vildan Softic, and Joan Yin. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!