const - JS | lectureJavaScript OOP

Inheritance Between "Classes" - Constructor Functions

JavaScript OOP

Over the last couple of lectures, we explored how prototypal inheritance works in JavaScript. And we did that using a couple of different techniques: constructor functions, ES6 classes, and the Object.create method. All of these techniques basically allow objects to inherit methods from its prototype. But now let's turn our attention to more real inheritance, like we learned in the very first lecture of this section.

When I say real inheritance, I mean inheritance between classes and not just prototypal inheritance between instances and a prototype property like we've been doing so far. And note that I'm using the class terminology here because it simply makes it easier to understand what we will do. But, of course, we already know that real classes do not exist in JavaScript.

In this lecture, we will create two new classes: Employee and Engineer. And we will make the Engineer class inherit from the Employee class. So, the Engineer class will be the parent class, and the Employee class will be the child class. That's because Engineer is basically a subtype of Employee.

This is really useful because, with this inheritance setup, we can have specific methods for an Engineer, but then the Engineer can also use generic methods from the Employee class. And that's basically the idea of inheritance that we will implement in this lecture.

inheritance overview

Just like before, we will start by implementing this using constructor functions. It might feel a bit difficult to understand at first, but I promise you that it will get easier as we go along. This will allow you to understand exactly how we set up the prototype chain in order to allow inheritance between the prototype properties of two different constructor functions. Then in the next lecture, we will do the same thing using ES6 classes, which, as you would expect, is a lot easier. And finally, of course, we will go back to using the Object.create method as well.

So, let's get started. First, let's create our parent Employee constructor function. This parent class will have the properties name, dateOfBirth, department, and a calculateAge method added to its prototype property.

index.js
const Employee = function (name, dateOfBirth, department) {
  this.name = name;  this.dateOfBirth = dateOfBirth;  this.department = department;};

Employee.prototype.calculateAge = function () {
  console.log(2022 - this.dateOfBirth);
};

Now, let's create our child Engineer constructor function. Usually, we want a child class to have the same functionality as the parent class but with some additional functionality. Hence, we usually pass on the same arguments and some additional ones.

index.js
const Engineer = function (name, dateOfBirth, department, projects, position) {
  this.name = name;  this.dateOfBirth = dateOfBirth;  this.department = department;  this.projects = projects;
  this.position = position;
};

Let's now create a new engineer object.

index.js
const johnson = new Engineer(
  'Shawn Johnson',
  1990,
  'Engineering',
  ['Microservices', 'Security'],
  'Senior Engineer'
);

console.log(johnson);

engineer object

Let's now add a method to our Enginner prototype property called present.

index.js
Engineer.prototype.present = function () {
  console.log(
    `Hello, my name is ${this.name}. I am a ${this.position} in the ${this.department} department.`
  );
};

Let's now call the present method on our johnson object and see what happens.

index.js
johnson.present();

engineer present

It works great! However, there is one thing that we can and should improve in our Engineer constructor function. Right now, the above-highlighted section in the Engineer constructor function is basically a simple copy of the Employee constructor function. And as you know, having duplicate code is never a good idea. First, because it violates the DRY principle, and second and even worse, imagine that the implementation of the Employee constructor function changes in the future, then that change will not be reflected in the Engineer constructor function.

So instead of having the duplicate code, let's simply call the Employee function from within the Engineer function and pass in the arguments.

index.js
const Engineer = function (name, dateOfBirth, department, projects, position) {
  Employee(name, dateOfBirth, department);  this.projects = projects;
  this.position = position;
};

Do you think this will work? Let's try it out.

engineer object error

Arggh! We get an error 🫤. The reason why we are getting this error is because we are now calling the Employee function as a regular function. And remember that the this keyword inside a regular function call is set to undefined. Hence that's why we get the above error.

So instead of simply calling the Employee function, we need to manually set the this keyword as well. So do you remember how we can call a function and at the same time set the this keyword inside that function? Well, we can simply use the call method. So let's do that.

index.js
const Engineer = function (name, dateOfBirth, department, projects, position) {
  /**   * 1. new Engineer(...) creates a new empty object   * 2. The this keyword inside the Employee Engineer call is set to that empty object   * 3. The this keyword is passed to the Employee function as the first argument of the call method   * 4. The Employee function then sets the properties on the empty object   */  Employee.call(this, name, dateOfBirth, department);  this.projects = projects;
  this.position = position;
};

If you now execute your code again, you will see that it works perfectly fine. Hence, this is a much better and more robust solution.

So far, this is what we have built:

engineer-constructor-function

It's simply the Engineer constructor function, its prototype property, and then the johnson object linked to its prototype. And that prototype is, of course, the constructor function's prototype property. Now, this link between instance and prototype has been made automatically by creating the johnson object using the new keyword. All of this is what we've already learned, so there is nothing new at this point.

Now, an engineer is also an employee. So we want Engineer and Employee to be connected like this:

engineer-employee-connection

So we want the Engineer class to be the child class and inherit from the person class, which will then function as the parent class. This way, all instances of Engineer could also get access to methods from the Employee prototype property, like the calculateAge method, through the prototype chain. And that's the whole idea of inheritance.

Looking at the above diagram, basically what we want to do is to make Employee.prototype the prototype of Engineer.prototype. Or in other words, we want to set the __proto__ property of Engineer.prototype to Employee.prototype.

We will have to create this connection manually. And to do this we will have to use the Object.create method. Because defining prototypes manually is exactly what Object.create does. Enough talking, let's do it.

index.js
/**
 * Make sure to write the below-highlighted code before the present method declaration.
 * I will explain why later.
 */

const Engineer = function (name, ...) {/**/};

Engineer.prototype = Object.create(Employee.prototype);
Engineer.prototype.present = function () {
 /**/
};

With this, the Engineer.prototype object is now an object that inherits from Employee.prototype. Now, the reason I asked you to create this connection before adding any method to the Engineer.prototype object is because, Object.create will return an empty object. Hence, at the moment we create this connection, the Engineer.prototype object is empty. And so then, on to that empty object, we can add methods like the present method.

Whereas, if we did it the other way around, and added the present method to the Engineer.prototype object first, then the Object.create method would basically overwrite the present method.

Now you might be wondering why we even needed to use the Object.create method in the first place? Why couldn't we simply write the following code?

index.js
Engineer.prototype = Employee.prototype;

This seems logical, but it will not work because if we do this, we will end up with the following:

engineer-employee-prototype

The image on the right is the result of the above code. It is a completely wrong prtototype chain. It is because we are actually saying that the Employee prototype property and the Engineer prototype property should be the same object. And that's not what we want. We want the Engineer prototype object to be the prototype of Employee.prototype. So, we want to inherit from it, but it should not be the same object. And that's exactly why we need to use the Object.create method.

With all this in place, we should be able to use the calculateAge method on the johnson object.

index.js
johnson.calculateAge(); // 32

Let's now analyze what exactly happened here...

We already know that this worked because of the prototype chain. But let's see exactly how from the below diagram.

inheritance-prototype-chain

From the diagram, when we do johnson.calculateAge(), we are effectively doing a property or method lookup. In this case, as we know, the calculateAge method is not directly on the johnson object. It is also not on johnson's prototype because that's where we defined the present method and not the calculateAge method. So, just like before, whenever we try to access a method that's not on the object's prototype, JavaScript will go up the prototype chain and look for it. And that's exactly what happened here. JavaScript will finally find the calculateAge method in Employee.prototype, which is exactly where we defined it.

That's the whole reason why we set up the prototype chain like this. So that the johnson object can inherit whatever methods are in its parent's class. In summary, we are now able to call a method that is on an Employee prototype property on an Engineer object, and it still works.

Still, from the diagram, we can see that Object.prototype sits at the top of the prototype chain. And this means that we could still call a method that is on the Object.prototype property on the johnson object. For example, we could call the hasOwnProperty method. It doesn't matter how far away in the prototype chain a method is.

To finish this lecture, let's now check all we just said above.

Let's start by checking the __proto__ property of the johnson object.

index.js
console.log(johnson.__proto__);

johnson-prototype

We can see that the __proto__ property of the johnson object is the Engineer.prototype object. We can clearly see that it contains the present method. But also, it says it's of type Employee instead of Engineer. We will fix this later.

Let's move up one step further again and check __proto__.__proto__.

index.js
console.log(johnson.__proto__.__proto__);

johnson-prototype-prototype

We indeed get the Employee.prototype object which contains the calculateAge method.

Let's now fix the __proto__ property of the johnson object. Ideally, checking Engineer.prototype.constructor should point back to the Engineer function. But it doesn't. It points to the Employee function. So, we need to fix this.

index.js
console.dir(Engineer.prototype.constructor); // Employee

The reason we are having this is because we set the prototype property of Engineer using the Object.create method. Hence, this makes that the constructor of Engineer.prototype is still Employee. We need to fix this because, sometimes, it is important to rely on the constructor property.

To fix this, all we have to do is to set the constructor property to Engineer.

index.js
Engineer.prototype.constructor = Engineer;