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.
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() + "!"; }
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.
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.