"super" in Javascript: An In-Depth Guide

"super" in Javascript: An In-Depth Guide

Introduction

With the introduction of ES2015, Javascript introduced the class syntax that uses traditional prototypal inheritance under the hood but provides a more robust and declarative way of writing object-oriented code.

When working with inheritance using the class syntax, one of the things we come across is the keyword: super. This article is all about super; Why do we need it? How do we use it? How it actually works?

super keyword is used to refer to the methods and properties defined inside the super-class.

There are 3 ways the super keyword can be used:

  • Inside the constructor of a sub-class
  • Inside the methods of a sub-class
  • Inside methods that are defined inside an object literal

Let's get into the details of different usages of the super keyword.

super inside Constructors

Inside a sub-class, we need to invoke super as though it were a function, before we can access this inside the constructor. We will get an error if we don't call super() before accessing this.

class A { /* code */ }

class B extends A {
  constructor() {
    this.prop = 123;  // can't access "this"
    super();
  }
}

new B();  // error

We need to call super() inside the constructor before we can access this and this is because the object isn't created until super() is called. This ensures that the object being constructed is initialized by the super-class before the sub-class gets a chance to initialize the newly-created object.

You can write other code inside the constructor before calling super() but that code shouldn't try to access this. You also need to call super() before the constructor ends; Not calling super() at all will result in an error.

How super calls the super-class constructor?

Consider the following code example:

class Person { /* code */ }

class Student extends Person { /* code */ }

When we use the extends keyword to extend another class, extends keyword does two things:

  • it sets the SuperClass.prototype as the prototype of the SubClass.prototype
  • it also sets the super-class constructor as the prototype of the sub-class constructor

We can verify the above two points using our code example:

class Person { /* code */ }

class Student extends Person { /* code */ }

Object.getPrototypeOf(Student.prototype) === Person.prototype // true
Object.getPrototypeOf(Student) === Person // true

extends keyword sets up two prototype chains in our code:

prototype chains made by extends keyword

When super() is called inside the constructor of a sub-class, super keyword uses the prototype of the sub-class' constructor to access the super-class constructor and calls it. We can verify this by modifying the prototype of the Student constructor.

class PersonA {
  constructor() {
    console.log("PersonA constructor called");
  }
}

class PersonB {
  constructor() {
    console.log("PersonB constructor called");
  }
}

class Student extends PersonA { 
  constructor() {
    super();
  }
}

// change the prototype of the "Student"
// constructor to "PersonB"
Object.setPrototypeOf(Student, PersonB);

new Student();  // "PersonB constructor called"

Student class extends PersonA class but just before creating an instance of Student, we changed the prototype of the Student constructor to PersonB. At the end, when we create an instance of Student, super() inside the Student constructor called the constructor defined in PersonB class instead of the constructor defined in PersonA class. (Try commenting the second last line of code and observe which constructor gets called).

super inside Methods in Classes

In traditional object-oriented languages like Java, functions defined inside classes are known as methods. In Javascript, the term "method" was loosely used to refer to a function assigned to a property of an object. With the introduction of ES2015, there's a distinction between methods and functions; code inside true methods can access super; code inside traditional functions cannot use super.

Let's verify this with the following code example:

class A {
  foo() {
    console.log("foo method in class A");
  }
}

class B extends A {
  baz() {
    super.foo();
  }
}

B.prototype.bar = function () {
  super.foo();  // error: cannot access "super" inside a function
}

In the above code example, baz is a method, so it can access super to call the foo method defined in the super-class A. On the other hand, bar is not a true method; it is a function assigned to a property bar on the B.prototype object. As a result, we cannot use super inside it. (I will explain why we cannot use super inside regular functions later in this article).

Inside the methods of a sub-class, we can use super to refer to the properties and methods defined in the prototype of the super-class.

Inside a sub-class, if a method needs to call a method inside the super-class as part of its implementation, we can use super to refer to the method in the super-class.

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

  introduce() {
    return `My name is ${this.name}`;
  }
}

class Student extends Person {
  constructor(name, id) {
    super(name);
    this.id = id;
  }

 // uses super class' method as part of 
 // its implementation
  introduce() {
    console.log(`${super.introduce()} and i am a student`);
  }
}

const student = new Student("Jack", "123");
student.introduce();

super.introduce() refers to the introduce method inside the super-class, i.e. Person.

How super accesses the prototype of the super-class?

Earlier i said that i will explain why we cannot use super inside traditional functions assigned to properties of an object. Reason is that true methods have a link to the object on which they are defined; traditional functions assigned to object properties do not have this link.

Each method has an internal field named [[HomeObject]] which contains a reference to the object on which that particular method is defined.

[[HomeObject]] is defined in the ecmascript specification as:

If the associated function has super property accesses and is not an ArrowFunction, [[HomeObject]] is the object that the function is bound to as a method. The default value for [[HomeObject]] is undefined.

Methods use [[HomeObject]] field to refer to the object on which they are defined. super relies on this [[HomeObject]] internal field on methods to refer to the properties and methods in super-class.

Consider the following code example:

class Parent {
  helloParent() {
    console.log("hello from Parent class");
  }
}

class Child extends Parent {
  helloChild() {
    super.helloParent();
  }
}

const childObj = new Child();
childObj.helloChild();  // "hello from Parent class"

Inside the helloChild method of the Child class, we called the helloParent method defined inside the Parent class using the super keyword.

Let us understand how a property or a method lookup using super is resolved. We will use the above code example and go over the steps taken to access the helloParent method from inside of the helloChild method of the Child class.

As mentioned before, [[HomeObject]] refers to the containing object. In the case of the helloChild method of the Child class, containing object is the Child.prototype. So [[HomeObject]] field on helloChild method refers to the Child.prototype object.

Steps taken to access helloParent using super.helloParent are as follows:

  1. Using the [[HomeObject]] internal field, get the object on which helloChild method is defined: in our case, that object is Child.prototype.
  2. Get the prototype of the object obtained in the first step: in our case, prototype of Child.prototype is the Parent.prototype.
  3. Finally, we call the helloParent method on the object obtained in step 2. If the method named helloParent didn't exist, Javascript would have thrown an error.

Following diagram shows the links between protoypes and the link of [[HomeObject]] field of helloChild method to its containing object, i.e. Child.prototype.

Screenshot from 2021-02-24 17-19-52.png

Why super needs [[HomeObject]] field?

In the previous example, when we called super.helloParent() inside helloChild method, super used the [[HomeObject]] field to get the containing object, i.e. Child.prototype and then got its prototype, i.e. Parent.prototype.

It seems that super is just getting the prototype of this.prototype. So why it relies on the [[HomeObject]] internal field when it can just get the prototype of the current object's prototype?

Let us understand the need of the [[HomeObject]] field with the help of the following code example:

class Parent {
  hello() {
    console.log("hello from Parent class");
  }
}

class Child extends Parent {
  hello() {
    super.hello();
  }
}

class GrandChild extends Child {
  hello() {
    super.hello();
  }
}

const grandChildObj = new GrandChild();
grandChildObj.hello(); // "hello from Parent class"

In the above code example, grandChildObj.hello() will call the hello method in the GrandChild class. hello method in GrandChild class calls super.hello().

Instead of using the [[HomeObject]] field, if super just gets the prototype of the current object's (grandChildObj) prototype (GrandChild.prototype), then we will get the Child.prototype - everything is good so far!

Let's see what happens when the hello method in the Child class gets called as a result of calling super.hello() from inside of the hello method of the GrandChild class.

Inside the hello method of the Child class, we call super.hello() to call the hello method of the Parent class. Again, instead of using [[HomeObject]] field, if super gets the prototype of the current object's (grandChildObj) prototype (GrandChild.prototype), then we will get Child.prototype. Surely, you see a problem here!

Instead of getting the Parent.prototype object, we got the Child.prototype object because current object is still grandChildObj and prototype of its prototype is Child.prototype. As a result, super.hello() inside the hello method of the Child class will lead us to an infinite recursion until we get a stack overflow error (try replacing super with this keyword in the above example).

This is why super relies on the [[HomeObject]] field. Instead of getting the prototype of the current object's prototype, it needs to get the prototype of the object on which the current method is defined.

In our code example, prototype of the current object inside the hello method of the Child class is Child.prototype. The object on which hello method of the Child class is defined is Child.protoype and its prototype is Parent.prototype which is what we need inside the hello method of the Child class for super to correctly call the hello method of the Parent class.

If you got lost in the above explanation, following diagrams show you the difference in using and not using the [[HomeObject]] field.

why super needs [[HomeObject]] field why super needs [[HomeObject]] field

super inside Methods in Object Literals

ES2015 introduced the syntax for defining methods in object literals.

Methods defined in this way are different from the traditional functions in two ways:

  • they don't have a prototype property; this means that they can't be called as constructors
  • they are true methods, meaning the code inside them can use super
const obj = {
  foo: function() {
    console.log("i am a function, can't access 'super' inside me");
  },
  bar() {
   console.log("i am a method, can access 'super' inside me");
  }
};

console.log(typeof obj.foo.prototype) // object
console.log(typeof obj.bar.prototype) // undefined

Following code example shows the usage of super inside a method in an object literal:

const obj = {
  toString() {
    return super.toString().replace("Object", "obj");
  }
};

console.log(obj.toString()); // "[object obj]"

toString method in obj calls the toString defined in Object.prototype using super and returns a string after replacing some part of the string, returned by super.toString(), with "obj".

If we wanted to write the above code example before ES2015, we would have explicitly called the toString method in Object.prototype. Following code shows how the above code example could have been written before ES2015.

const obj = {
  toString: function () {
    return Object.getPrototypeOf(this)
                 .toString.call(this)
                 .replace('Object', 'obj');
  }
};
console.log(obj.toString()); // "[object obj]"

Not only this code is more verbose, we also have to deal with explicitly setting this whereas in the earlier version, super handled it for us.

Summary

super keyword can be used inside:

  • constructor of a sub-class
  • methods defined in classes
  • methods defined in object literals

In sub-classes, super needs to be invoked as a function to create the object and give the super-class a chance to initialize the object before sub-class can access and initialize the newly created object.

Inside methods, super relies on the [[HomeObject]] internal property to access the methods and properties defined in the prototype of the super-class.

Did you find this article valuable?

Support Yousaf Khan by becoming a sponsor. Any amount is appreciated!