Class Inheritance in JavaScript

The class notation allows you to create an inheritance system as well. You can extend a class to another class using the extends keyword. For example, here is a User class:

javascript
1class User {
2  constructor(name) {
3    this.name = name;
4
5    this.firstName;
6    this.lastName;
7  }
8
9  get name() {
10    return `${this.firstName} ${this.lastName}`;
11  }
12
13  set name(name) {
14    [this.firstName, this.lastName] = name.split(" ");
15  }
16}
17
18let user = new User("John Doe");
19
20console.log(user);
text
1User { firstName: 'John', lastName: 'Doe' }

And then, you might need a Profile class that extends to User:

javascript
1class Profile extends User {
2  create() {
3    console.log("Profile created.");
4  }
5}
6
7let profile = new Profile("John Doe");
8
9console.log(Object.getPrototypeOf(profile));
text
1User {}

As you can see, the prototype of profile is set to be User. You can access the create() method from profile, but not user:

javascript
1profile.create(); // -> Profile created.
2user.create(); // -> Error
text
1Profile created.
2/Users/. . ./index.js:29
3user.create();
4     ^
5
6TypeError: user.created is not a function
7    at Object.<anonymous> (/Users/. . ./index.js:29:6)
8    . . .
9
10Node.js v21.6.0

Technically, you don't need to know how this works internally, but if you are interested, when you use extends to create a class, a prototype property will be set to that class.

Recall that classes are just constructor functions, and functions are a special type of objects, which means you can assign properties to functions, as explained in the linked article above.

For example,

javascript
1class Profile extends User {
2  create() {
3    console.log("Profile created.");
4  }
5}
6
7console.log(Profile.prototype);

When an object is constructed with the new keyword, the class that prototype points to will automatically become the object's prototype, which is User in this case.

Overriding a method

Consider this scenario, in the User class, you have a create() method in charge of creating a new user in the database. Of course, we haven't yet discussed how to work with databases using JavaScript, so for now, we'll use console.log() as a simple replacement, just for demonstration purposes.

javascript
1class User {
2  constructor(name) {
3    this.name = name;
4
5    this.firstName;
6    this.lastName;
7  }
8
9  get name() {
10    return `${this.firstName} ${this.lastName}`;
11  }
12
13  set name(name) {
14    [this.firstName, this.lastName] = name.split(" ");
15  }
16
17  create() {
18    console.log("User created.");
19  }
20}

And in the Profile class, you need another create() method to add more user details, such as the bio, profile picture, and so on.

javascript
1class Profile extends User {
2  create() {
3    console.log("Profile created.");
4  }
5}

JavaScript does allow you to create the same method in both the parent and child classes, but by default, the one in the parent class will be overridden. In this case, User.create() will be overridden by Profile.create().

javascript
1let profile = new Profile("John Doe");
2
3profile.create();
text
1Profile created.

However, in this example, you might not want to replace the parent method completely. Because in a real project, the new user row must be created in the database, before you can add extra information to it.

The former would be the job for User.create(), and the latter would be done by Profile.create(). So, a better solution would be letting the Profile.create() build on top of User.create() instead of replacing it completely.

You can access the methods in the parent class using the keyword super. For example:

javascript
1class Profile extends User {
2  create() {
3    super.create();
4    console.log("Profile created.");
5  }
6}
7
8let profile = new Profile("John Doe");
9
10profile.create();
text
1User created.
2Profile created.

In this case, super.create() will execute the User.create() method, and then continue to the rest of the Profile.create() method.

Overriding the constructor

You may have noticed that the inheritance system we just went over is missing a very important feature. What if you need to add more properties in the child class? At first, you might want to add a constructor() in the child class like this:

javascript
1class User {
2  . . .
3}
4
5class Profile extends User {
6  constructor(email, name) {
7    this.email = email;
8    this.name = name;
9
10    . . .
11  }
12}

But things are a bit more complex than that. When you execute this constructor, JavaScript returns an error.

text
1/Users/. . ./index.js:24
2    this.email = email;
3    ^
4
5ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
6    at new Profile (/Users/. . ./index.js:24:5)
7    . . .
8
9Node.js v21.6.0

It is telling you that you must call super before accessing this in the child constructor, meaning this is what you have to do:

javascript
1class Profile extends User {
2  constructor(name, email) {
3    super(name);
4    this.email = email;
5  }
6
7  create() {
8    super.create();
9    console.log("Profile created.");
10  }
11}
12
13let profile = new Profile("John Doe", "me@example.com");
14
15console.log(profile);
text
1Profile { firstName: 'John', lastName: 'Doe', email: 'me@example.com' }

super() executes the constructor() in the User class, and then continue to the rest of Profile.constructor().

In summary, in JavaScript classes, super() refers to the constructor() of the parent class, and super.create() refers to the create() method inside the parent class. And when extending a constructor(), always make sure super() is called before using the this keyword.

The instanceof operator

JavaScript also allows you to check if an object is created by a class using the instanceof operator. For instance, in the following example, user is created by executing new User(), so it is an instance of the class User.

javascript
1class User {
2  constructor(name) {
3    this.name = name;
4
5    this.firstName;
6    this.lastName;
7  }
8
9  get name() {
10    return `${this.firstName} ${this.lastName}`;
11  }
12
13  set name(name) {
14    [this.firstName, this.lastName] = name.split(" ");
15  }
16
17  create() {
18    console.log("User created.");
19  }
20}
21
22let user = new User("John Doe");
23
24console.log(user instanceof User);
text
1true

If a class extends to another class, for example, Profile extends to User,

javascript
1class User {
2  constructor(name) {
3    this.name = name;
4
5    this.firstName;
6    this.lastName;
7  }
8
9  get name() {
10    return `${this.firstName} ${this.lastName}`;
11  }
12
13  set name(name) {
14    [this.firstName, this.lastName] = name.split(" ");
15  }
16
17  create() {
18    console.log("User created.");
19  }
20}
21
22class Profile extends User {
23  create() {
24    super.create();
25    console.log("Profile created.");
26  }
27}

The objects created by Profile are also considered an instance of User.

javascript
1let profile = new Profile("John Doe");
2
3console.log(profile instanceof Profile);
4console.log(profile instanceof User);
text
1true
2true