JavaScript Under the Hood #2: Sub-classes

Understanding sub-classes and inheritance in JavaScript

Keeping the momentum from my last article, this article aims to explain how sub-classes work in JavaScript.

Sub-classes are what make inheritance possible, which is one of the core aspects of the OOP paradigm. This allows us to maintain the behavior of one object in another without the need to do the same work again.

JavaScript inheritance can be a bit confusing because our subclass will not "inherit" the properties from its parent, it will just have access to the same fields, so even if inheritance is the standard term be careful to not be confused with this.

So how does sub-classing work? Let’s show with code:

function employeeCreator(name) {
  const newEmployee = Object.create(employeeFunctions);
  newEmployee.name = name;
  return newEmployee;
}
const employeeFunctions = {
  sayName: function () {
    console.log(`My name is ${this.name}.`);
  },
};
const employee = employeeCreator("Alice");

employee.sayName(); // My name is Alice.

function developerCreator(name, language) {
  const newDeveloper = employeeCreator(name);
  Object.setPrototypeOf(newDeveloper, developerFunctions);
  newDeveloper.language = language;
  return newDeveloper;
}

const developerFunctions = {
  code: function () {
    console.log(`${this.name} is coding with ${this.language}.`);
  },
};

Object.setPrototypeOf(developerFunctions, employeeFunctions);

const developer = developerCreator("Bob", "JavaScript");

developer.sayName(); // My name is Bob.
developer.code(); // Bob is coding with JavaScript.

Here we want to maintain the employee properties in developer without duplication, so we created an inheritance manually using employeeCreator as the parent class and developerCreator as a sub-class.

We achieve this by creating a new employee with the employeeCreator, then we extend this employee object with developer functionalities using the method setPrototypeOf, then we can access any developer attributes from the employee object, and with the help of object.setPrototypeOf which let us have access to another object properties by getting it’s prototype and with this we can get the sayName into the developerFunctions and all of this in the newDeveloper.

This "manual" approach to inheritance in JavaScript, however, is not the recommended way since it’s not the most readable or performant way to do It. So let’s step up a bit by using the new keyword.

function Employee(name) {
  this.name = name;
}

Employee.prototype.sayName = function () {
  console.log(`My name is ${this.name}.`);
};

const employee = new Employee("Alice");

employee.sayName(); // My name is Alice.

function Developer(name, language) {
  Employee.call(this, name);
  this.language = language;
}

Developer.prototype = Object.create(Employee.prototype);

Developer.prototype.code = function () {
  console.log(`${this.name} is coding with ${this.language}.`);
};

const developer = new Developer("Bob", "JavaScript");

developer.sayName(); // My name is Bob.
developer.code(); // Bob is coding with JavaScript.

As you can see, some things changed. Now we can use the new keyword to create a new object (instead of using Object.create) and we also can call the constructor function Employee to initialize its properties and add functions to EmployeeCreator.prototype.

In Developer we use call (instead of Object.setPrototypeOf) to use the parent constructor on this new object we’re creating, and after that, we add a language value to this. To keep up with Employee functionalities, we pass its prototype to Developer.prototype by creating an object with it.

The downside of this approach is it is verbose and still not very readable. So the JavaScript team tried to fix this with the next solution:

class Employee {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(`My name is ${this.name}.`);
  }
}

const employee = new Employee("Alice");

employee.sayName(); // My name is Alice.

class Developer extends Employee {
  constructor(name, language) {
    super(name);
    this.language = language;
  }

  code() {
    console.log(`${this.name} is coding with ${this.language}.`);
  }
}

const developer = new Developer("Bob", "JavaScript");

developer.sayName(); // My name is Bob.
developer.code(); // Bob is coding with JavaScript.

Now we’re using the ES2015 approach, as I said in my class article the class solution is just syntactic sugar to the last solution and works in the same way, but it’s cleaner and easier to read if compared to the others.

For the Developer the this keyword is uninitialized, so we must call super (which replaces the Object.call) before we call this. This happens because our object is only later instanced and returned in the Employee to be automatically assigned to this in the Developer, so we can’t just call this at this point, but only after the super. It’s something like this = super(name).

But how does super knows it needs to run Employee? It’s just because of the extends keyword, which replaces the need to add functions "behind the scenes", directly in the prototype. Also, it sets a reference to the Employee constructor in the Developer __proto__ in a property on Objects that points to the prototype set in them.

And that’s the class approach. This is what makes it possible to emulate an object-oriented language with a prototype-based environment.

Thanks for reading, if you want to improve your knowledge from here you can check the references for more.

References:

https://frontendmasters.com/courses/object-oriented-js
https://262.ecma-international.org/9.0/#sec-ordinary-and-exotic-objects-behaviours
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto
https://www.youtube.com/watch?v=DqGwxR_0d1M

We want to work with you. Check out our "What We Do" section!