Class is not a class. Extends doesn’t Extend in JS

Tushar Mohan
7 min readJun 6, 2020

--

Photo by Clément H on Unsplash

Apologies for the weird title to this post. This certainly is not click-bait if you realize that ES6 ‘classes and extends’ are not the same as their homonyms from other languages such as Java (classical). I aim to help you form a mental model around this ES6 feature.

We could sum up the whole post in literally three (if not two 😬) lines of code. We will begin by observing the legacy way of creating objects in JS and for that, we need to understand constructors in JS.

Constructors

Classical object oriented languages use constructors to initialize objects after they have been allocated memory. While JS object constructors are used to both prepare objects (create new execution contexts) and initialize them. This is done by using ‘constructor functions’.

  • Constructor functions are technically regular functions but, they start with an uppercase letter (by convention) and are executed using the ‘new’ operator.
  • When the ‘new’ operator is used, a fresh execution context (think as an initialized object in a classical OO language) is created and added to the execution stack. Any reference to ‘this’ within the constructor function, will point to the newly created execution context.
  • Using the new operator, we have the ability to create multiple contexts/instances of the same object.
// Example of a basic constructor function. 
// * notice that we follow convention of naming it with the first
// character in uppercase.
function Human(name, age) {
this.name = name;
this.age = age;
}
console.log(new Human('ABC', 22)); //Human { name: 'ABC', age: 22 }// What if we exclude the 'new' operator? 🤔
console.log(Human('ABCD', 22)); // undefined

Since we did not use the new operator, the second console did not create a new context and add it to the stack. Instead, it executed the function normally assigned the values to the current context instead.

This is how we create new objects in JS. The newly created objects can be assigned to variables and stored for performing operations at a later point in time.

Let us now implement the same thing with class.

// * notice that we follow convention of naming it with the first
// character in uppercase.
class Human {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
console.log(new Human('ABC', 22)); //Human { name: 'ABC', age: 22 }// What if we exclude the 'new' operator? 🤔🤔🤔
console.log(Human('ABCD', 22)); // TypeError: Class constructor Human cannot be invoked without 'new'

With ES6, the compiler was kind enough to tell us that we have invoked a Class constructor without the new operator. Even though the object definition code looks very different and gives an illusion of implementing classes in a classical OOP manner, under the hood, it is our earlier snippet that gets wrapped in a syntactic sugar. Therefore, a (JS) class is not a class(in classical OOP).

// Let us make our non ES6 snippet also throw an error when invoked without 'new'function Human(name, age) {
if (!new.target) {
return new Error("Class constructor Human cannot be invoked without 'new'");
}
this.name = name;
this.age = age;
}
console.log(Human('ABC', 22)); // Error: Class constructor Human cannot be invoked without 'new'

If you are reading this, I assume that you are now convinced that a ‘Class is not a class’ in JS. Lets now see why ‘Extends doesn’t Extend in JS’. In a classical OOP setup, extends is used to inherit from a ‘base class’. For languages that do not use the extends keyword like Objective C, Swift, and C++, they use a syntax similar to the snippet below, but logically they achieve inheritance using a similar strategy.

class SomeSubclass: SomeSuperclass {
// subclass definition goes here
}

Yes! You get the idea. We are discussing classical vs prototypal inheritance. Based on our recent agreement about classes in JS, we’ll ‘extend’ (😬) the Human class from earlier discussions to see the magic. However, we need to understand ‘prototypes’ first.

Prototypes

Since JS is all about objects, these objects have a hidden ‘[[Prototype]]’ property. Since this is hidden, we have a getter/setter defined in JS to access it. We access this ethereal property by using ‘__proto__’. It is encouraged that we use ‘Object.getPrototypeOf’ as a getter and ‘Object.setPrototypeOf’ being the setter.

One more thing before we see the code. We will encounter two weirdly similar and relevant things.

  1. .__proto__
  2. .prototype

The visible difference is that while ‘__proto__’ is available on the instances of a class, while ‘prototype’ is a property on a class constructor. So, the property ‘__proto__’ on instances point to the ‘prototype’ property available on the constructor of that instance. Let us verify this an example.

function Human(name, age) {
this.name = name;
this.age = age;
}
const human1 = new Human('ABC', 22);
const human2 = new Human('XYZ', 21);
console.log(Object.is(human1.__proto__, Human.prototype)); // true
console.log(Object.is(human2.__proto__, Human.prototype)); // true
console.log(Object.is(human2.__proto__, human1.__proto__)); // true

The __proto__ acts as a link to its constructor’s prototype. This ability allows us to introduce new methods and properties on a constructor’s ‘prototype’ object and allow all the instances (of the same or subtype) to access them.

When we access a property on an object, and if it is not found immediately on the instance, JS looks up the object’s ‘prototype’. Prototype is an object and as we know, all JS objects have a ‘[[Prototype]]’ property, JS keeps looking (by going higher in the prototype chain) for the property that we accessed initially until it reaches the base ‘JS Object’ or finds the intended property somewhere in the ‘chain’. These chains are formed when we associate the prototypes of objects using our constructor functions. And this is what is referred to as ‘prototype chaining’.

function Human(name, age) {
this.name = name;
this.age = age;
}
const human1 = new Human('ABC', 22);
const human2 = new Human('XYZ', 21);
Human.prototype.talk = function () {
console.log(`Hey! I am ${this.name}. How are you?`)
};
human1.talk(); // Hey! I am ABC. How are you?
human2.talk(); // Hey! I am XYZ. How are you?

This chaining is often deliberate (governed by our code) and at times implicit (in case of creation of objects like ‘{ … }’ , where the [[Prototype]] is links to the root JS Object).

Lets say we now have to add Humans who are Musicians. How do we achieve that without ES6?

function Human(name, age) {
this.name = name;
this.age = age;
}
Human.prototype.talk = function () {
console.log(`Hey! I am ${this.name}. How are you?`);
};
function Musician(name, age, instrument) {
if (!new.target) {
return new Error(
"Class constructor Musician cannot be invoked without 'new'"
);
}
Human.call(this, name, age);
this.instrument = instrument;
}
Musician.prototype = Object.create(Human.prototype); // 1
Musician.prototype.constructor = Musician; // 2
const musician1 = new Musician('GHI', 23, '🎸');
const musician2 = new Musician('MNO', 20, '🥁');
musician1.talk(); // Hey! I am GHI. How are you?

The actual inheritance is achieved in two lines (thus my claim in the beginning). One is -

Musician.prototype = Object.create(Human.prototype);

The Object.create creates a new object (and returns) after assigning Human.prototype to the new object’s (hidden) ‘[[Prototype]]’ property. We then associate Musician (our class constructor)’s prototype property to as the returned object from our call to Object.create.

While we achieve that, we also borrow the ‘constructor’ property of Human.prototype. Not adding the line

Musician.prototype.constructor = Musician;

while creating new objects using the Musician constructor, we will end up creating objects using the Human constructor. We access the Musician’s prototype and reset the constructor property to what it originally was by re-assigning a reference to our Musician constructor function.

Using Object.create(Human.prototype), we create links. Since the method returns a new object with a link to the ‘[[Prototype]]’ property, we can use the object to specify properties that we want to exist on a specific class without exposing them on the base class’ prototype.

These also allow us to override (not really 🧐) the properties that are already defined somewhere higher in the prototype chain.

function Human(name, age) {
this.name = name;
this.age = age;
}
Human.prototype.talk = function () {
console.log(`Hey! I am ${this.name}. How are you?`);
};
function Musician(name, age, instrument) {
if (!new.target) {
return new Error(
"Class constructor Musician cannot be invoked without 'new'"
);
}
Human.call(this, name, age);
this.instrument = instrument;
}
Musician.prototype = Object.create(Human.prototype);
Musician.prototype.constructor = Musician;
const musician1 = new Musician('GHI', 23, '🎸');
const human2 = new Human('XYZ', 21);
Musician.prototype.talk = function () {
console.log(`Hey! I am ${this.name} and I play ${this.instrument}. Wassup?`);
};
musician1.talk(); // Hey! I am GHI and I play 🎸. Wassup?
human2.talk(); // Hey! I am XYZ. How are you?

In the snippet above, instead of overriding, we ‘shadow’ the property ‘talk’. JS looks up the prototype chain only if the property was not found on the object or the JS Object (the root of everything in JS ecosystem) is reached. On adding the ‘talk’ to Musician’s prototype, JS finds the object and does not traverse the remaining chain. Thus, we see the message ‘Hey! I am GHI and I play 🎸. Wassup?’ on console. Isn’t that super cool? 🤩

Compare this against how we achieve the same thing in ES6 using class and extends keywords.

class Human {
constructor(name, age) {
this.name = name;
this.age = age;
}
talk() {
console.log(`Hey! I am ${this.name}. How are you?`);
}
}
class Musician extends Human {
constructor(name, age, instrument) {
super(name, age);
this.instrument = instrument;
}
talk() {
console.log(
`Hey! I am ${this.name} and I play ${this.instrument}. Wassup?`
);
}
}
const musician1 = new Musician('GHI', 23, '🎸');
const human2 = new Human('XYZ', 21);
musician1.talk(); // Hey! I am GHI and I play 🎸. Wassup?
human2.talk(); // Hey! I am XYZ. How are you?

This is cleaner, more readable and concise. But under the hood, it is the same old JS!

So the next time you come across snippets such as the one below,

class Greet extends React.Component {
render() {
return <h1>Hello, World!</h1>;
}
}

you will know that it is not the classical object oriented approach to inheritance. Instead, an insanely amazing method of achieving the same.

Cheers!

--

--

Tushar Mohan

Full time: Autodidact. Learner. Developer. Occasionally: Drummer/Photography enthusiast. Currently: Engineering Manager at Zomato.com