Class Inheritance in ECMAScript 6

Tuesday, November 25, 2014

Continuing from a previous post

Over the years, there have been various approaches to building class hierarchies in JavaScript. All these approaches worked by stitching together constructor functions and prototype objects and each approach was slightly different from the others.

The ES6 standard allows for a declarative, consistent approach to inheritance using the class and extends keywords. As an example, let’s start with a simple class to represent a person with a name.

class Person {
    
    get name() {
        return this._name;
    } 

    set name(newName){
        if(newName){
            this._name = newName;
        }
    } 

}

var p1 = new Person();
p1.name = "Scott";
expect(p1.name).toBe("Scott");

Now we also need a class to represent an employee who has both a name and a title. Using the extends keyword, we can have an Employee class inherit from the Person class, meaning every object instantiated as an Employee will also have all the methods and properties available for a Person.

class Employee extends Person {

    get title() {
        return this._title;
    }

    set title(newTitle) {
        this._title = newTitle;
    }
}

var e1 = new Employee();
e1.name = "Scott"; // inherited from Person
e1.title = "Developer";

expect(e1.name).toBe("Scott");
expect(e1.title).toBe("Developer");
expect(e1 instanceof Employee).toBe(true);
expect(e1 instanceof Person).toBe(true);

When describing the relationship between Employee and Person we often say Person is a super class of Employee, while Employee is a sub class of Person. We can also say that every employee is a person, and as you can see in the above code, the instanceof check in JavaScript confirms that an object created using the Employee class is also an instance of Person.

While every Employee object will inherit all the state and behavior of a Person, the Employee class also has the ability to override the methods and properties defined by Person. For this example, let’s add a doWork method to every Person.

class Person {

    get name() {
        return this._name;
    }

    set name(newName){
        if(newName){
            this._name = newName;
        } 
    }

    doWork() {
        return this.name + " works for free";
    } 

}

var p1 = new Person();
p1.name = "Scott";
expect(p1.doWork()).toBe("Scott works for free");

By default, an Employee will behave the same as a Person.

var e1 = new Employee();
e1.name = "Scott";
e1.title = "Developer";
expect(e1.doWork()).toBe("Scott works for free");

However, in the Employee class we can override the behavior of doWork by providing a new implementation.

class Employee extends Person {

    // ...

    doWork() {
        return this.name + " works for a salary";
    }
}

var p1 = new Person();
p1.name = "Scott";
expect(p1.doWork()).toBe("Scott works for free");

var e1 = new Employee();
e1.name = "Scott";
e1.title = "Developer";
expect(e1.doWork()).toBe("Scott works for a salary");

Now, as the tests prove, objects instantiated from the Employee class will behave slightly differently in the doWork method than objects instantiated from the Person class. In object-oriented programming, we call this behavior polymorphism.

Using Super

There are times when a class will want to invoke behavior in its super class directly, which can be done with the super keyword. For example, the Employee class could implement the doWork method as follows.

doWork() {
    return super() + "!";
}

Invoking super will execute the super class method of the same name, so with the above code a passing test would now look like the following.

var e1 = new Employee();
e1.name = "Alex";
e1.title = "Developer";

expect(e1.doWork()).toBe("Alex works for free!");

A class can also refer explicitly to any method in the super class.

doWork() {
    return super.doWork() + "!";
}

Super Constructors

One common scenario for using super is when inheriting from a class that requires constructor parameters. The following Person class now accepts a name parameter during initialization.

class Person {

    constructor(name) {
        this._name = name;
    }

    get name() {
        return this._name;
    }

}

Now an employee will need both a title and a name during initialization.

class Employee extends Person {

    constructor(name, title) {
        super(name);
        this._title = title;
    }

    get title() {
        return this._title;
    }

}

Notice how the Employee constructor method uses super to pass along the name parameter to the Person constructor and reuse the logic inside the super class. What’s interesting about JavaScript compared to other object oriented languages, is how you can choose if and when to call the super class constructor. If I were to pick a style to remain consistent, I’d always make a call to super in a sub class before executing any other logic inside the constructor. This style would keep JavaScript classes consistent with the behavior of other object-oriented languages.

The Caveat

Now that we know what inheritance looks like in ES6, it’s time for a word of caution. Inheritance is a fragile technique to achieve re-use that only works well in a limited number of scenarios. Do a search for “composition over inheritance” to see more details.


Comments
gravatar Phil Tuesday, November 25, 2014
Thanks for letting readings know that inheritance is normally a bad idea. Nevertheless, in my opinion, the "class" keyword should not be in ES 6 (ignoring the fact it has been reserved long before). JavaScript already has prototypical inheritance and should not be made to feel like Java more than it already is- Its not Java!. A mass of developer are diving into it and are trying to change the language to suit their inclinations. Don't get me started on TypeScript and CoffeeScript. If you want to learn to play a guitar, do not use a game pad for guitar hero or practice on an air-guitar; play a real guitar the way it was designed.
gravatar imma Wednesday, November 26, 2014
Nice clear explanation :-) I am quite sad that js (/ecmascript) is being twisted to be more like class-based languages rather than prototype-based and/or functional languages, but I can see how this would be more familiar to those coming from a class-based background I'm particularly upset by arrow functions, which are really awesome except for the fixed binding that stops code being re-used by composition :-( sorry for going a bit off topic - imma
gravatar imma Wednesday, November 26, 2014
ps : where'd my line breaks go to?
gravatar imma Wednesday, November 26, 2014
ah ... comments have the newlines in, they're just styled by default to hide non < br > breaks perhaps use : white-space: pre-wrap; ?
gravatar Mario Friday, January 2, 2015
I think this is huge for the future of Javascript. Those who disagree should remember that JS has been evolving into an Enterprise programming language both in the Front and Backends. There are structural complexities you can not just ignore when it comes to Design-pattern, Type-safety, and so on. Javascript is the most dynamic language out there; if you don't like or need a specific feature for your build, you can choose other styles available. These added features don't change the dynamic Object-oriented style of Javascript, it just makes more accessible to all different kinds of business logic for all different industries.
Comments are closed.

My Pluralsight Courses

K.Scott Allen OdeToCode by K. Scott Allen
What JavaScript Developers Should Know About ECMAScript 2015
The Podcast!