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 theSubClass.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:
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:
- Using the
[[HomeObject]]
internal field, get the object on whichhelloChild
method is defined: in our case, that object isChild.prototype
. - Get the prototype of the object obtained in the first step: in our case, prototype of
Child.prototype
is theParent.prototype
. - Finally, we call the
helloParent
method on the object obtained in step 2. If the method namedhelloParent
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
.
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.
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!