Unlike class-based languages, JavaScript uses prototypes for inheritance. The class syntax abstracts most of the prototypal inheritance model away, but I believe it’s important to understand how JavaScript really works. Let’s learn how to implement a “subclass” in JavaScript without using the class syntax or the extends keyword.

The base “class”

The base “class,” Hero, represents a character in a role-playing game (RPG). Every instance of the Hero class has two of its own properties (as distinct from inherited properties): .name and .level.

function Hero(name, level) {
this.name = name;
this.level = level;
}

We can use the new operator to invoke the Hero function as a constructor. This invokes the Hero function’s internal [[Construct]] method. When invoked as a constructor, the value of the this keyword inside the body of the Hero function is the current instance under construction.

let hero = new Hero("Bjorn", 1);
console.log(hero instanceof Hero); // true
console.log(Object.getPrototypeOf(hero) === Hero.prototype); // true

let entries = Object.entries(hero);
console.log(entries); // [["name", "Bjorn"], ["level", 1]]

The result is an instance of the Hero “class” with two of its own properties: .name and .level. The Hero instance inherits from the object that is assigned to the Hero.prototype property. Note that we haven’t added any instance methods yet.

The derived “class”

The derived “class,” Warrior, represents a specific type of hero. In addition to the .name and .level properties, every instance of the Warrior class has an extra own property: .weapon.

When a function is invoked as a constructor, the value of the new.target meta-property inside the function body is a reference to the constructor that the new operator was called upon. To make sure the value is correct, we can use the static Reflect.construct() method. This method is like the new operator, except it’s a function. It also lets us specify a different prototype for new.target.

function Warrior(name, level, weapon) {
let warrior = Reflect.construct(Hero, [name, level], Warrior);
warrior.weapon = weapon;
return warrior;
}

It may seem weird that we’re not using the this keyword anywhere inside the body of the Warrior function. However, it’s perfectly legal to return an object from a constructor. When the function is invoked with the new operator, the returned object is used. Because we’re using the Warrior “class” for the third parameter of the Reflect.construct() method, the returned object is still considered an instance of the Warrior “class.”

let warrior = new Warrior("Bjorn", 1, "axe");
console.log(warrior instanceof Warrior); // true
console.log(Object.getPrototypeOf(warrior) === Warrior.prototype); // true

Unfortunately, we still have work to do. If we check the prototypes of Warrior and Warrior.prototype, we’ll see that they are Function.prototype and Object.prototype, respectively.

Object.getPrototypeOf(Warrior) === Function.prototype; // true
Object.getPrototypeOf(Warrior.prototype) === Object.prototype; // true

For the Warrior “class” to be a true “subclass” of the Hero “class,” we need the prototype of Warrior to be Hero, and the prototype of Warrior.prototype to be Hero.prototype.

Linking the prototypes

To set the prototype of an object, we can use the static Object.setPrototypeOf() method. Because of the way JavaScript engines optimize prototypes, this is unfortunately a very slow operation. This is one advantage of the class syntax: it doesn’t force you to set the prototypes dynamically.

Object.setPrototypeOf(Warrior, Hero);
Object.setPrototypeOf(Warrior.prototype, Hero.prototype);

Now the Warrior “class” correctly inherits from the Hero “class.” The first line makes sure the static properties and methods are inherited, while the second line makes sure the instance properties and methods are inherited.

Adding instance methods

To share properties and methods among all instances of a “class,” we can add them to the object that is assigned to the “class’” .prototype property. Let’s add a .greet() method to the Hero “class” and an .attack() method to the Warrior “class.”

Hero.prototype.greet = function () {
return `${this.name} says hello.`;
};

Warrior.prototype.attack = function () {
return `${this.name} attacks with the ${this.weapon}.`;
};

Because we linked the prototypes, instances of the Warrior “class” have access to the methods of Warrior.prototype and the methods of Hero.prototype.

let warrior = new Warrior("Bjorn", 1, "axe");

console.log(warrior.greet()); // "Bjorn says hello."
console.log(warrior.attack()); // "Bjorn attacks with the axe."

console.log(Object.getPrototypeOf(warrior) === Warrior.prototype); // true
console.log(Object.getPrototypeOf(Warrior) === Hero); // true

The prototype chain

The Warrior “class”

The following series of Object.getPrototypeOf() calls shows what the prototype chain of the derived Warrior “class” looks like.

Object.getPrototypeOf(Warrior); // Hero
Object.getPrototypeOf(Hero); // Function.prototype
Object.getPrototypeOf(Function.prototype); // Object.prototype
Object.getPrototypeOf(Object.prototype); // null

A Warrior instance

The following series of Object.getPrototypeOf() calls shows what the prototype chain of an instance of the derived Warrior “class” looks like.

Object.getPrototypeOf(new Warrior()); // Warrior.prototype
Object.getPrototypeOf(Warrior.prototype); // Hero.prototype
Object.getPrototypeOf(Hero.prototype); // Object.prototype
Object.getPrototypeOf(Object.prototype); // null

Complete example

function Hero(name, level) {
this.name = name;
this.level = level;
}

function Warrior(name, level, weapon) {
let warrior = Reflect.construct(Hero, [name, level], Warrior);
warrior.weapon = weapon;
return warrior;
}

Object.setPrototypeOf(Warrior, Hero);
Object.setPrototypeOf(Warrior.prototype, Hero.prototype);

Hero.prototype.greet = function () {
return `${this.name} says hello.`;
};

Warrior.prototype.attack = function () {
return `${this.name} attacks with the ${this.weapon}.`;
};

Equivalent classes

class Hero {
constructor(name, level) {
this.name = name;
this.level = level;
}

greet() {
return `${this.name} says hello.`;
}
}

class Warrior extends Hero {
constructor(name, level, weapon) {
super(name, level);
this.weapon = weapon;
}

attack() {
return `${this.name} attacks with the ${this.weapon}.`;
}
}

Summary

  • To make a derived “class” inherit from a base “class,” you can use the static Reflect.construct() method.
  • You also need to link the prototypes with the static Object.setPrototypeOf() method.
  • The class syntax is easier to read, more performant, and less error-prone. However, it’s still useful to understand how JavaScript really works.