January 8, 20228 min read
Prototypal Inheritance and Classes in JavaScript
Hello everyone, in this article, we would be discussing:
- Constructor function,
- Prototypes,
- Inheritance, classes in JS
These are very important concepts and would help you to understand Object-Oriented Programming(OOP) in JS. So, keep reading till the end and I hope you'll learn something from it.
Objects
We had discussed earlier how to create and work with objects using literal notation here.
const phone = {
RAM: "8GB",
OS: "Andriod",
};
In practice, we often need to create many similar objects like a list of phones, employees, etc. So, we can achieve this by creating custom types in JS using Constructor Function and then creating multiple objects from it. In other programming languages, we generally use class
to define this kind of custom type but in JS, the class system is built directly using functions.
So, rather than using classes in JS directly, we can learn how to do the same using constructor functions
which is the base of Object-Oriented Programming in JS.
ES6 introduces the
class
keyword that allows us to define custom types. But, classes in JS are syntactic sugar over constructor functions with some enhancements.
Constructor Functions
Constructor functions are like regular functions with some conventions:
- These functions should be invoked with the
new
operator. - Naming of these functions are written in CamelCase(starting with capital letter e.g. Employee) by convention
- These functions should not have an explicit return value
function Employee(name) {
this.name = name;
this.role = "Developer";
}
And, this is to create an object using that constructor function
const employee = new Employee("Souvik");
console.log(employee); // Employee {name: "Souvik", role: "Developer"}
this
inside the function definition points to the object that has been created using the new
keyword in front of the constructor function while invoking it.
So, what if we don’t use the new
keyword while calling the function?
In that case, the function would be invoked as a regular function, a new object would NOT be created and returned. Let’s understand this part by invoking the function mentioned above without the new
operator:
const employee = Employee();
console.log(employee); // undefined
As you can see, undefined
would be returned which any regular function returns by default. Also, this
would refer to global object window
as the constructor function has been invoked as a regular function.
These are the followings the new
keyword is responsible for while invoking constructor function:
-
Create a new object and assign it to
this
-
Add properties to the object with the given value
-
Return the newly created object
this
keyword in JS
We had talked about this
keyword before and found out this
behaves differently based on implementation. There’re 4 ways to call a function and this
refers to a different object in each case.
-
If calling a constructor function, then
this
sets to the newly created object -
Invoking a function that belongs to an object would set
this
to the object itself, which is called Implicit binding as discussed here. -
Simply invoking a regular function would set
this
to the global objectwindow
. -
The last way of invoking a function allows us to set
this
ourselves usingcall()
,apply()
andbind()
methods - that’s known as Explicit binding, talked about it here earlier as well.
Prototypal Inheritance
The problem with the constructor function is that if there’s any method present in the constructor function then that will be created for every instance created using the constructor function.
function Employee(name) {
this.name = name;
this.role = "Developer";
this.printDetails = function () {
console.log(`${this.name} works as a ${this.role}`);
};
}
So, to make things memory efficient, we can add methods to the prototype
property of the constructor function, so that all instances of a constructor function can share the same methods.
function Employee(name) {
this.name = name;
this.role = "Developer";
}
Employee.prototype.printDetails = function () {
console.log(`${this.name} works as a ${this.role}`);
};
const employee = new Employee("Souvik");
employee.printDetails(); // Souvik works as a Developer
So, what is a prototype?
A prototype is just an object and all objects created from a constructor function are secretly linked to the prototype.
The prototype also keeps a reference to its own prototype object. And, prototype’s prototype is also linked to its own prototype and so on. This is how it forms prototype chain.
JavaScript uses this link between an object and its prototype to implement inheritance which is known as Prototypal Inheritance.
When we try to access a property or method of an object,
-
it tries to find that in the object’s own properties. Any properties or methods defined in the object itself get the highest precedence over defining the same elsewhere just like variable shadowing in the scope chain discussed here.
-
If it doesn’t get that within the object’s properties then it will try to find that in the object’s constructor’s prototype.
-
If it’s not there even in the prototype object, the JavaScript engine will continue looking up the prototype chain to get the value. At the end of the chain, there’s
Object()
's prototype: the top-most prototype object in the chain - if the property is not found even there, then the property isundefined
.
But, one question still arises, how is an object created by a constructor function secretly linked to its prototype?
The answer is any object created by a constructor function is linked to its prototype using the __proto__
property which is made by the constructor function and directly refers to the constructor function's prototype.
console.log(employee.__proto__ === Employee.prototype); // true
__proto__
has been used here just for learning purposes. Don't use or reassign the__proto__
property anywhere in production code as it's not supported by all browsers and updating its value might create performance issues since JS searches for properties along the prototype chain.
If we need to check the prototype for an object, we can use the Object.getPrototypeOf()
method for the same which takes an object as an argument and returns the prototype of that object.
console.log(Employee.prototype === Object.getPrototypeOf(employee)); // true
Object.create()
As we discussed, using the __proto__
property is not a good practice to use in code, so the same shouldn't be used to implement inheritance or build a prototype chain.
That's why ES5 introduced Object.create()
method to implement prototypal inheritance.
Object.create()
takes an object as an argument and returns a new object with its __proto__
set to the object that was passed as argument into Object.create()
.
const person = {
name: "Souvik",
greet: function () {
console.log(`Hi, I’m ${this.name}`);
},
};
const teacher = Object.create(person);
teacher.teach = function (subject) {
console.log(`I can teach ${subject}`);
};
teacher.greet(); // Hi, I'm Souvik
teacher.teach("JavaScript"); // I can teach JavaScript
console.log(Object.getPrototypeOf(teacher) === person); // true
We can leverage Object.create()
the following way to implement inheritance.
function Animal(name) {
this.name = name;
}
Animal.prototype.walk = function () {
console.log(`${this.name} can walk`);
};
function Dog(name, lifetime) {
Animal.call(this, name); // calling parent constructor function to initialize parent properties for child objects
this.lives = lifetime;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.details = function () {
console.log(`${this.name} can live for ~${this.lives} years`);
};
const dog = new Dog("Dobby", 10);
dog.walk(); // Dobby can walk
dog.details(); // Dobby can live for ~10 years
In this way Dog inherits properties and methods from Animal using prototypal inheritance. But this is a bit tricky and verbose.
That's why ES6 introduces the class
and extends
keyword to simplify inheritance implementation in JS. Classes in JS are special functions. And the same implementation using class
would look like this:
class Animal {
constructor(name) {
this.name = name;
}
walk() {
console.log(`${this.name} walks`);
}
}
class Dog extends Animal {
constructor(name, lifetime) {
super(name);
this.lives = lifetime;
}
details() {
console.log(`${this.name} can live for ~${this.lives} years`);
}
}
const dog = new Dog("Dobby", 10);
dog.walk(); // Dobby can walk
dog.details(); // Dobby can live for ~10 years
console.log(typeof Animal); // function
That's all 😀. Thanks for reading till now🙏.
If you want to read more on these, refer to OOP in JS MDN, Object Prototypes MDN, Inheritance in JS MDN, Classes MDN
Share this blog with your network if you found it useful and feel free to comment if you've any doubts about the topic.