DEV Community

Cover image for Prototypical Inheritance in JavaScript
Lalit Aditya
Lalit Aditya

Posted on

Prototypical Inheritance in JavaScript

Prototypical inheritance is one of the core concepts in JavaScript.

But at the same time it is considered as one of the tough topics to understand. Even the experienced JS developers fall short in confidence while dealing with inheritance/prototypical inheritance in JS.

But to be honest, prototypical inheritance doesn't have any room for ambiguity, and it's pretty straightforward to understand.

In this post, I will deep dive into the core concepts of JS related to prototype. What exactly is a prototype? What is the role of the new keyword in prototypical inheritance, and why is this inheritance called prototypical inheritance? Internal working and ES6 updates regarding prototypical inheritance.

Table of Contents

  1. The Confusion
  2. new keyword and its role in prototypical inheritance
  3. Objects in JS
  4. Inheritance chain (instance and static)
  5. ES6 updates

The Confusion:

  • The biggest confusion that almost everyone faces while trying to understand the prototypical inheritance is the word prototype itself.


    Because this is actually confusing, as per the ECMA docs, the word prototype is used in two different places, and that too, to represent two different entities altogether


    One is the [[prototype]], and the other is the property named prototype

  • Now let's settle the dust.

    1. [[prototype]] refers to the __proto__ property. Every object in JavaScript has a default, built-in property named __proto__. And the same __proto__ property represents the [[prototype]] mentioned in the docs. For the sake of simplicity, in this post, whenever I want to refer to this [[prototype]], I will always refer to it as __proto__.
    2. The property named prototype - In JavaScript, every function created by default has a property named prototype attached to its object. (Yes, a function is also an object in JavaScript, but a special kind of object; we will discuss the specialty of this object later in this post.)


    Think of it like a function is an object, and it has a key named prototype on it. We didn't add this key to the function object; it's present by default. And the value of this key is again an object.

So a function object in JS has 2 properties by default.
One is __proto__ because a function is also an object, and every object has __proto__
And the other is prototype because it's a function.

In simple terms, only function objects get access to a special default key named prototype along with __proto__

This might sound confusing; let's take an example

function add(a, b) {
  return a+b
}
Enter fullscreen mode Exit fullscreen mode

When the JS engine sees this instruction, it actually creates an object in the background. And since this object is a function object, as discussed, it will have a default property added to it named prototype

Something like this:

{
  name: add, // name of the function
  length: 2, // referring to 2 parameters the function add has
  prototype: { // this key is added by default as add is a function
     // .... (some default key-value pairs would be present, we will see them later)
     __proto__:Object.prototype // wait what ? what is this ? 
  }
  __proto__: Function.prototype // again wait what ? what is this ?
}
Enter fullscreen mode Exit fullscreen mode

NOTE:

Wherever we see an object ({}), we see a __proto__ being present, because every object in JS has a __proto__ property

Now, add is just a reference to this object present in memory, and using add we can access this object.

For example, we can add custom key-value pairs to this object.

add.myCustomKey = 10;
add.myCustomFn = function() { console.log('Add custom function') }
console.log(add.myCustomKey) // 10
add.myCustomFn();  // Add custom function
Enter fullscreen mode Exit fullscreen mode

NOTE:

If you observed, there were a couple of places where __proto__ was used above in the function object, and we had no idea why at one place Object.prototype is present and why at the other place Function.prototype is present. And that's completely fine. We will clear up this confusion in the coming sections.

  • To summarize, every object in JavaScript has a built-in property named __proto__ and since a function is also an object in JavaScript, even the function object also has a __proto__ property, but as discussed, since a function is a special object, apart from the built-in __proto__ property, it also has one more special property named prototype, which is again an object.

Mental Model:

  • If object, then has access to the __proto__ property.
  • If the object is a function object, then it has access to the __proto__ and prototype properties.
let obj = {}
function add(a, b) {
  return a+b;
}
console.log(obj.__proto__) // available
console.log(add.__proto__) // available
console.log(add.prototype) // available because add is a function
console.log(add.prototype.__proto__) // avaialbe, because prototype itself is an object, and every object has __proto__ property
// All these properties are present by deafult. Added by langugage
Enter fullscreen mode Exit fullscreen mode
  • So from now on in this post, when __proto__ is referred to, we are talking about the default property present in every object, and when prototype is referred to, we are talking about the default property present in a function object.

new keyword and its role in prototypical inheritance:

  • The new keyword in JavaScript is used to create an object.
    Syntax: new fn()

    Here, fn refers to a function or a constructor function, and the new keyword must be used with a function; otherwise, it results in an error.

  • But most importantly in this process of object creation, it also sets the __proto__ property of the newly created object.

  • We still don't know what exactly this __proto__ property is. But we learned the fact that it exists by default on every object. We will see what exactly this __proto__ is in the next section

  • Internal working of new:
    When new fn() is executed, there are some predefined sets of activities that it performs. They are:

  1. Create a brand new object
  2. Bind this to the newly created object.
  3. Set the __proto__ of the newly created object to fn's prototype.
  4. Run the function and return the newly created object (implicit return) if no explicit return is mentioned in fn.

Let's understand it by an example.

function User(name, age) {
    this.name = name;
    this.age = age;
}
const u = new User("Peter", 25);
console.log(u); 
Enter fullscreen mode Exit fullscreen mode

Now let's try to simulate the work done by the new keyword.

So think of something like below. (Not exactly, but more of a pseudocode)

function User(name, age) {
    // const obj = {}; // 1. creation of a new object
    // this = obj     // 2. binding this to new object created
    // this.__proto__ = User.prototype // 3. sets the newly created object __proto__ to User.prototype
    this.name = name;
    this.age = age;
    // return this;  // 4. implict return added, new object returned
}
Enter fullscreen mode Exit fullscreen mode

So, as we can see, the new keyword plays an important role by not only creating a new object but also setting the created object's __proto__ property.

We will see the significance of this step in the next section and how this step lays out the foundation for prototypical inheritance

Now I assume there shouldn't be any doubt/confusion in User.prototype. Since User is a function object, it will have access to a property named prototype.

console.log(User.prototype) // {} 
Enter fullscreen mode Exit fullscreen mode

Objects in JS:

  • In the previous 2 sections, we learned that every object in JavaScript has access to an inbuilt property named __proto__ and if the object is a function object, then along with __proto__ it will also have access to prototype


    But what exactly are these predefined properties? What values do these properties hold? What's their significance?


    Let's try to answer that.

  • In simple terms, __proto__ defines where JavaScript should continue looking when a property is not found on an object itself.


    When you try to access something on an object and it doesn’t exist there, JavaScript follows the __proto__ link to another object and continues the search.


    And that's it; that's the whole purpose of __proto__ in JS.

  • But since __proto__ is a default, built-in property present in every object, it also has a default value, and it is Object.prototype.


    But now what is this Object.prototype ?
    Before answering it, let's look at a few examples

const n = new Number(10);
const s = new String('Peter');
const p = new Promise((resolve) => resolve({data: 'success'}));
function User(name, age) {
  this.name = name;
  this.age = age;
}
const u = new User('Peter', 25);
Enter fullscreen mode Exit fullscreen mode

Exercise: Try to perform all 4 activities done by the new keyword.

-    Were you able to find any common observation? I think from the above code snippet, the easiest instruction to understand would be const u = new User('Peter', 25), as we clearly discussed this case while we were trying to understand the new keyword

-    Here, "User" is a function or, more specifically, a constructor function since it is invoked via the new keyword. Hey, but wait, we also learned that the syntax of the new keyword is new fn(), where fn is a function

-    Then how are we able to do new Number(10), new String('Peter'), new Promise((resolve) => resolve({data: 'success'}))? Why does the new keyword work here?

-    The idea is actually simple: it works because, under the hood, all of these are constructor functions. In fact, every built-in type you use—Number, String, Promise, Boolean, Array and others—is essentially a predefined constructor function provided by the language itself. And hence the new keyword works.

We can check this by the following:

console.log(typeof Number);  // function
console.log(typeof String);  // function
console.log(typeof Promise); // function
console.log(typeof User);    // function
Enter fullscreen mode Exit fullscreen mode
  • Now the two most important built-in types provided by the language are Object and Function. And these are constructor functions.


    From previous sections, we learned that every function object will have a built-in property named prototype.


    Hence we can do the below

console.log(typeof Object)    // function
console.log(typeof Function)  // function
console.log(Object.prototype)  // {}  
console.log(Function.prototype) // {}

Enter fullscreen mode Exit fullscreen mode

    Object.prototype and Function.prototype aren't actually empty objects; why we see empty objects is because all the properties of Object.prototype and Function.prototype are non-enumerable, hence the console.log doesn't show them

    But we can check the properties of these objects by the following:

console.log(Object.getOwnPropertyDescriptors(Object.prototype)
console.log(Object.getOwnPropertyDescriptors(Function.prototype)
Enter fullscreen mode Exit fullscreen mode

    The above instructions give all the properties present in the Object and Function predefined functions.

  • Now let's get back to the default value of __proto__,. As discussed, the default value is Object.prototype


    Meaning, if the property being searched for isn't present in the object, continue the search in Object.prototype, because that's what is present in __proto__. For ex:

const obj = {};
console.log(obj.__proto__ === Object.prototype) // true
console.log(obj.toString()) // '[object Object]'
Enter fullscreen mode Exit fullscreen mode

     But wait, in the above code snippet, I never defined any property named toString on my obj, but still somehow we can access the toString property

     And what we just discovered is the prototypical inheritance in JS

     Let's dissect the flow

When we tried to access toString on obj, the JS engine first searched toString on obj. Since toString wasn't present in obj, the JS engine checked the __proto__ of obj, it had the value of Objet.prototype, so now the JS engine continued the search in Obect.prototype. In Object.prototype, we find the key toString whose value is a function, and invoking it gave us the result '[object Object]'

NOTE:

  1. What if toString didn’t exist on Object.prototype? The idea is straightforward: JavaScript would continue the lookup using the same mechanism—by following the __proto__ chain. It would check the __proto__ of Object.prototype and keep searching upward.

  2. __proto__ property would be present in Object.prototype as Object.prototype is an object, and every object in JS will have a __proto__ property

  3. The catch, however, is that __proto__ on Object.prototype is null, which signals the end of the prototype chain—there’s nowhere further to look, so the search stops there.

  • So from the above discussion, we understand a couple of things.


    1. The default value of __proto__ in any object is Object.prototype.

    2. The lookup stops if the property isn’t found on Object.prototype, because its __proto__ is null—signaling the end of the prototype chain.

  • Let's take one more example to understand:

const obj = {}
console.log(obj.name) // undefined
Object.prototype.name = 'Parker'
console.log(obj.name) // Parker
Enter fullscreen mode Exit fullscreen mode

-    The above example should clear up how the lookup chain works. Let's try to simulate the JS engine.

-    obj is an object, and hence it will have a default built-in property named __proto__ with value Object.prototype

-    Upon accessing obj.name, search a property named name on obj. If found, return the value; else, continue the search by checking the value of the __proto__ property of obj.

-    Since the name property was not found on obj, continue the search in Object.prototype (because that's what is present in obj.__proto__)

-    Check for the name property on Object.prototype, if not found, continue the search by checking the value of the __proto__ property inside Object.prototype. The value found is null, meaning stop the lookup chain and return undefined

-    But after the first console.log, we are doing something interesting; we are actually adding a property in Object.prototype. So the Object.property should look something like this

Object.prototype

-    Hence in the 2nd time lookup, we actually get a value of Parker as this time it is found in Object.prototype

  • Now let's discuss the most interesting part. Remember, in the earlier sections, we said that functions are also objects in JS, but they are a special kind of object. But why is that?


    We learned a rule that every object in JS will have a __proto__ property, and its value will be Object.prototype. But this isn't true in the case of function objects.


    For every function object (either custom or predefined), __proto__ is equal to Function.prototype. This can be easily checked as follows.

function wish() {
   console.log('Hi');
}
console.log(wish.__proto__ === Function.prototype)     // true
console.log(wish.__proto__ === Object.prototype)       // false
console.log(Number.__proto__ === Function.prototype)   // true
console.log(Promise.__proto__ === Function.prototype)  // true
console.log(Function.__proto__ === Function.prototype) // true
console.log(Object.__proto__ === Function.prototype)   // true

// the last 2 cases are also true, because Function, Object are also functions in JS. This can be checked using typeof operator
Enter fullscreen mode Exit fullscreen mode
  • Now this should also explain why call, apply, and bind functions can be invoked on every function in JS.


    The reason is they exist in Function.prototype

console.log(Object.getOwnPropertyDescriptors(Function.prototype)) // call, apply, bind are properites inside Function.prototype
Enter fullscreen mode Exit fullscreen mode
  • Let's understand by taking one more example:
function wish() {
   console.log(`${this.name} says Hi`);
}
let obj1 = { name: 'Peter' };
wish.call(obj1);
Enter fullscreen mode Exit fullscreen mode

-    Let's again try to simulate the JS engine when wish.call(obj1) is invoked.

-    We know the function wish creates an object, and wish is a reference to the object.

-    When a property named call is accessed on wish, first the property call is searched in the object referred to by wish. It isn't present, so continue the search by checking the __proto__ of the object referred to by wish.

-    Since wish is a function object, its __proto__ has the value Function.prototype, so the lookup is continued in Function.prototype
-    In Function.prototype, we find the property call which is a function, and hence the wish.call is executed.

  • The final mental model:


    1. If an object then has access to the __proto__ property whose value is Ojbect.prototype

    2. If the object is a function object, then it has access to the __proto__ property and prototype property, and the value of __proto__ is Function.prototype.

The chain

Observations:

  1. Since wish,Function, and Object are functions (wish being custom-defined and Function, Object being predefined), they have access to both __proto__ and prototype properties, whereas obj is just a normal object and hence has access to only the __proto__ property.
  2. Every function object's __proto__ is Function.prototype and normal object's (obj's) __proto__ is Object.prototype.
  3. The prototype property of a function object always has a predefined property named constructor whose value is the reference to the function itself (check the above diagram)
  4. wish.prototype, Function.prototype, and Object.prototype are normal objects and not function objects, and hence they have the __proto__ value as Object.prototype
  5. Object.prototype.__proto__ is null—this is defined by the language itself. It marks the end of the prototype chain, which is why Object.prototype is considered the topmost level, and property lookup stops here.
  6. This process—where a property is first searched on the object itself, and if not found, the lookup continues along its [[prototype]] (__proto__ property) chain—is precisely why it’s called Prototypical inheritance.
  • One final example:
console.log(Function.x)
Enter fullscreen mode Exit fullscreen mode

-    Let's become the JS engine again and try to look up the property x

-    On the Function object, there is no property named x, so check the __proto__ property of Function and continue the search there. The __proto__ of Function is Function.prototype

-    Now let's go to Function.prototype. In Function.prototype also there is no property named x. Check the __proto__ in Function.prototype and go there. The __proto__ of Function.prototype is Object.prototype

-    Now check the Object.prototype. There is no property named x in Object.prototype, so check the __proto__ of Object.prototype. It's null. So stop the lookup and return undefined

(Check the above diagram for any confusions.)

Inheritance chain (instance and static):

The previous section could be heavy, but once that is cleared, there is not much left to understand in Prototypical inheritance.

In this section, let's see how to implement inheritance in JavaScript (via [[prototype]])

In the new keyword section, we saw that new keywords set the newly created object's [[prototype]] (__proto__ property) to the constructor function's prototype

Ex:

function RateLimiter(limit) {
  this.limit = limit;
  this.requests = 0;
}

let user1RateLimiter = new RateLimiter(5);
let user2RateLimiter = new RateLimiter(10);
Enter fullscreen mode Exit fullscreen mode

Now user1RateLimiter and user2RateLimiter are two objects from the RateLimiter constructor function

Exercise: Try to run the 4 activities done by the new keyword

  • In the memory, 2 objects are created, and each object will have its own copy of limit and requests, because the new keyword creates a new object and returns it (implicit return). This can be checked by following
user1RateLimiter.limit++;
user2RateLimiter.limit++;
console.log(user1RateLimiter.limit) // 6
console.log(user2RateLimiter.limit) // 11
Enter fullscreen mode Exit fullscreen mode
  • Now what if I need to add a method named isRequestAllowed to check whether the current request should be allowed or not? Basically, we need to check whether the current request number is less than the defined limit or not.


    For this, we need to define a method, but where should we define that method? The only condition is this method should be available to all the instances created out of the constructor function.

  • What if we define the method isRequestAllowed in the RateLimiter function object itself? Will this suffice for our requirement?


    Check the following:

methods present on function

  • Clearly this doesn't work, because when we try to access the method isRequestAllowed on the instance user1RateLimiter (run the algorithm we learned in the previous section), we get undefined.


    Because we first search the property isRequestAllowed on the user1RateLimiter object, since it's not found, we check the __proto__ which has the value RateLimiter.prototype (the new keyword sets this), and even in RateLimiter.prototype there is no property named isRequestAllowed, propagating the search to RateLimiter.protoytpe.__proto which points to Object.prototype. Since Object.prototype also doesn't have any property named isRequestAllowed, the search propagates to Object.prototype.__proto__ whose value is null, hence leading to undefined.

  • So defining the method on the function object itself didn't help us. The correct place to put these methods would be RateLimiter.prototype, the reason being all the objects created from the constructor function RateLimter will have the same __proto__ i.e, RateLimiter.prototype, and hence defining the method in RateLimiter.prototype will make sure that each instance created from RateLimiter can access the method.

methods present in the prototype

console.log(user1RateLimiter.__proto__ === user1RateLimiter.__proto__); 
// true, because both point to RateLimiter.prototype because of new keyword
console.log(user1RateLimiter.isRequestAllowed === user2RateLimiter.isRequestAllowed) // true, because it's the same method both objects reach in chain
Enter fullscreen mode Exit fullscreen mode
  • Now let's extend the same concept to achieve Parent-Child relationship in JavaScript. Consider the following example:
function Notification() {}

Notification.prototype.formatMessage = function (msg) {
  return `[Notification]: ${msg}`;
}

function EmailNotification() {}

EmailNotification.prototype.send = function (msg) {
  return `Email sent with message: ${msg}`;
};

const emailNotification = new EmailNotification();
console.log(emailNotification.send("Hello")); // Email sent with message: Hello
console.log(emailNotification.formatMessage("Hey there!")); // undefined
Enter fullscreen mode Exit fullscreen mode

-    It would be clear why the 2nd console.log gives undefined, simply because the formatMessage property was never found in the prototype chain (EmailNotification.prototype)

-    But we want the formatMessage method defined on the Notification function prototype to be available on the emailNotification instance

-    Meaning we want to establish a relationship between the EmailNotification function and the Notification function, so when a property isn't found on Notification.prototype, the current next searched place is Object.prototype, because Notifcation.prototype.__proto__ is Object.prototype

-    Current Relationship:

No Parent-Child relationship

-    To reach the target, we just need to do a minor tweak to EmailNotification.prototype. We want the chain to look like emailNotification -> EmailNotification.prototype -> Notification.prototype -> Object.prototype -> null

-    The magic line that will allow us to do this is:

EmailNotification.prototype = Object.create(Notification.prototype)
Enter fullscreen mode Exit fullscreen mode

By default, every object (except function objects) has its __proto__ pointing to Object.prototype. However, if you want to create an object with a custom prototype, Object.create() allows you to do exactly that by explicitly setting the object’s __proto__.

-     So Object.create(Notification.prototype) will create a new object whose __proto__ would be equal to Notification.prototype. And this new object's reference is assigned to EmailNotification.prototype

Parent-child relationship

-    The only downside is, as discussed in the previous section, the prototype object present in the function will always have a default key named constructor in it. But due to Object.create(Notification.prototype), we created an entirely new object, hence lost the constructor key, so as a good practice, let's add it back

EmailNotification.prototype = Object.create(Notification.prototype)
EmailNotification.prototype.constructor = EmailNotification
Enter fullscreen mode Exit fullscreen mode

-    And that's it; the above 2 lines will help us create the parent-child relationship (inheritance) in JavaScript

  • The inheritance we observed above can be described as instance-based inheritance, since the lookup chain is followed through the instance’s prototype (__proto__).

  • Now let's understand static inheritance.


    Strictly speaking, JavaScript didn’t have an explicit concept of static inheritance before ES6. With the introduction of classes in ES6, the static keyword formalized this idea, enabling inheritance of static methods and properties across classes.


    The idea is simple: instance inheritance works by looking up the [[Prototype]] (__proto__) chain starting from the instance created by the constructor function.


    In contrast, static inheritance works by following the [[Prototype]] (__proto__) chain on the function itself (function object)—rather than on its instances.

  • Let's take an example to understand:

function Animal() {}

Animal.identify = function () {
  return "I am an animal";
};
Enter fullscreen mode Exit fullscreen mode

-    If observed closely, we aren't defining the identify method in the Animal.prototype object; rather, we are defining this method on the Animal function object itself, and these are what are considered static methods in ES6.

-    Now let's define the child class:

function Cat() {}
console.log(Cat.identify); // undefined
console.log(Cat.identify()); // cannot invoke function on undefined
Enter fullscreen mode Exit fullscreen mode

-    I hope at this point, we are clear why Cat.idenify returns undefined. Because the property identify was not found in the function Cat [[prototype]] chain (__proto__ chain). Search order: Cat -> Cat.__proto__ = Function.prototype -> Function.prototype.__proto__ = Object.prototype -> null

No static relationship

-    Now what we want is property identify to be available to the function Cat, meaning we have to tinker with the __proto__ chain such that, once not found in Cat, the next search should happen in Animal (and not in Function.prototype).
-    The magic line that will allow us to achieve this is:

Cat.__proto__ = Animal
Enter fullscreen mode Exit fullscreen mode
console.log(Cat.identify);    // [Function]
conosole.log(Cat.identify()); // I am an animal
Enter fullscreen mode Exit fullscreen mode

-    By doing the above, we are manipulating the __proto__ chain of Cat to search next in the Animal function object

Static relationship

-    And what we witnessed above is called static inheritance in JavaScript.

ES6 Updates:

Awesome that you have reached until here; what we learned in the previous sections officially completes the prototypical inheritance in JavaScript.

But ES6 released a set of new features that allows us to establish inheritance in an easier way.

New keywords introduced in ES6 regarding prototypical inheritance: class, static, extends, and super.

1. The class keyword:

  • Strictly speaking, the class keyword is just syntactic sugar for constructor functions.

  • This was mainly for the developers who came from other ecosystems like Java, C#, etc. But under the hood it's the same constructor function.

  • Example:

class User {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    wish() {
       console.log(`${this.name} says Hi`);
    }

    static isValidAge(age) {
       return age >= 0 && age <= 120;
    }
} 
const u = new User('Peter', 25);
console.log(u.name);  // Peter
console.log(u.age);   // 25
user.wish();          // Peter says Hi
User.isValidAge(18);  // true
Enter fullscreen mode Exit fullscreen mode

    Under the hood, the above class becomes:

function User(name, age) {
    this.name = name;
    this.age = age;
}

User.prototype.wish = function() {
    console.log(`${this.name} says Hi`);
}

User.isValidAge = function(age) {
    return age >= 0 && age <= 120;
}
Enter fullscreen mode Exit fullscreen mode

-    i) The code written inside a class constructor essentially becomes the body of the underlying constructor function.

-    ii) The instance method defined in the class (wish) is added to the User.prototype (so that instances can have access to this method)

-    iii) The static method defined in the class (defined via the static keyword) is added directly as a property on the constructor function (User function object).

  • The new keyword works the same even for classes. The same 4 activities will be performed by the new keyword in the background.
  • Classes in JavaScript, by default, will run in strict mode. And can be invoked only via the new keyword.

2. The extends keyword:

  • The extends keyword is used to establish the parent-child relationship between classes.

  • Example:

class Animal {
  speak() {
    return "generic animal sound";
  }

  static describe() {
    return "Base class for all animals";
  }
}

class Cat extends Animal {
  meow() {
    return "Meow!";
  }

  static identify() {
    return "Cat class";
  }
}

const cat = new Cat();
console.log(cat.meow());       // Meow!
console.log(Cat.identify());   // Cat class
console.log(cat.speak());      // generic animal sound
console.log(cat.describe());   // Base class for all animals
Enter fullscreen mode Exit fullscreen mode
  • As we can see, the child class instance (cat) can access properties and methods defined in the parent class (Animal). This is made possible by the extends keyword, which establishes the inheritance relationship between them.

    But what it does under the hood is what we do manually in the case of constructor functions.

// set instance inheritance
Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat

// set static inheritance
Object.setPrototypeOf(Cat, Animal)
Enter fullscreen mode Exit fullscreen mode

    We don't need to do the above manually; extends does it on behalf of us.

3. The super keyword:

  • The super keyword allows us to call the parent class constructor function to perform the initialization of the child object.


    Yes, you heard it right—calling super() simply initializes the child class instance using the parent’s constructor; it does not create a separate parent class object.

  • This might sound surprising, especially to the devs coming from a Java background, where invoking super() first creates the parent class object, but that isn't true in the case of JavaScript

  • Because remember, all the new syntax that came in ES6 is just syntactic sugar for the traditional construction functions. Invoking super() simply means calling the parent constructor function by passing the child's this.

  • Let's understand it by an example:

class Animal {
  constructor(name) {
    this.name = name;
    console.log("Animal constructor called");
  }
}

class Cat extends Animal {
  constructor(name, color) {
    super(name); // calls parent constructor on the SAME object
    this.color = color;
    console.log("Cat constructor called");
  }
}

const cat = new Cat('Yapapa cat', 'gold');
console.log(cat.name);
console.log(cat.color);
Enter fullscreen mode Exit fullscreen mode

    Output:

Animal constructor called
Cat constructor called
Yapapa cat
gold
Enter fullscreen mode Exit fullscreen mode
  • Under the hood, the above classes convert to the below constructor functions
function Animal(name) {
  this.name = name;
  console.log("Animal constructor called");
}

function Cat(name, color) {
   Animal.call(this, name);     // important: Calling Animal function with Cat's this
   this.color = color;
   console.log("Cat constructor called");
}

// setting both instance and static inheritance between Animal and Cat
Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat;
Object.setPrototypeOf(Cat, Animal);

const cat = new Cat('Yapapa cat', 'gold');
console.log(cat.name);
console.log(cat.color);
Enter fullscreen mode Exit fullscreen mode
  • Now we know that because of the new keyword (new Cat('Yapapa cat','gold')), a new object would be created, and this binds to the new object created, and the same object is passed in Animal.call, and hence on the cat instance, the name property is added (by the Animal constructor function).

  • A natural question that can arise is, why even initialize the properties in the parent class, as the instance is always of the child class, and why not directly perform the initialization of the instance in the child constructor function itself?


    The answer is simple: To not violate the DRY principle, the main purpose of creating a parent class/parent constructor function is to keep the common code together


    A parent class/constructor function can have many child classes/constructor functions, so instead of initializing all the properties in the child class constructor itself, a better practice is to perform the initialization of common properties in the parent class/constructor function so that we don't have to repeat the initialization logic in each child class/constructor function


    Example:

class Vehicle {
  constructor(brand, year) {
    this.brand = brand;
    this.year = year;
  }
}

class Bike extends Vehicle {
  constructor(brand, year, type) {
    super(brand, year); // common initialization
    this.type = type;
  }

}

class Car extends Vehicle {
  constructor(brand, year, doors) {
    super(brand, year); // common initialization
    this.doors = doors;
  }
}
Enter fullscreen mode Exit fullscreen mode

    As we could, we extracted out the common initialization logic to the parent class, saving ourselves from repeating, and super in ES6 is a cleaner way to achieve this instead of Car.call(this, brand, year)


This ends the post. I hope this post added value to your JS learning journey. If yes, please do leave a comment/reaction.

Top comments (0)