Inheritance with JavaScript prototypes - LogRocket Blog
Orignally published at Logrocket Blog here.
TL;DR : In this post, we will look at prototypes and how to use them for inheritance in JavaScript. We will also see how the prototypical approach is different from class-based inheritance.
Inheritance
Inheritance, a prominent feature of a programming language, emerged with the introduction of object-oriented programming languages. Most of these languages were class-based languages. Here, class is like a plan or blueprint and objects are its manifestation. Meaning, in order to create an object, first we have to create a class. Then we can create any number of objects from one class.
Imagine, we have a class that represents a smartphone. This class has features like capturing images, GPS, etc, like any other smartphone. Here’s an example of how to create such a class and an object in C++ :
We created a class named SmartPhone
and it has a method named capturePictures
, to capture images.
Let’s imagine, we need an iPhone class, that would capture images along with some special features like a face ID scan. Here are two possible solutions:
- Rewrite the
captureImages
feature along with other common smartphone features, plus iPhone specific features into a new class. But this approach takes more time, effort and can introduce more bugs. - Reuse features from the
SmartPhone
class. This is where inheritance comes into play. It’s a way to reuse features from other classes/objects.
Here is how we can inherit capturePictures
method from the SmartPhone
class, in our new Iphone
class, in C++ :
Above is a trivial example of inheritance. However, it shows that inheritance allows us to reuse code in a way that the resulting program is less error-prone and takes less time to develop.
Here are some important things to know about classes :
- A class that inherits the feature is called as a child class
- A class from which features are inherited is called a parent class
- A class can inherit from multiple classes at once. For instance, class C inherits from class A and class B
- We can have multiple levels of Inheritance. For instance, class C inherits from class B and, class B inherits from class A
It’s worth to note that, class in itself is not doing anything. Until you create an object from a class, no work is actually done. We will see why it’s different from JavaScript.
Click here to see the full demo with network requests
What is a prototype?
In JavaScript, all objects have a special internal property which is basically a reference to another object. This reference depends upon how the object is created. In ECMAScript/JavaScript specification, it is denoted as [[Prototype]]
.
Since [[Prototype]]
is linked to an object, that object has its own [[Prototype]]
reference. This is how a chain is built (it’s known as the prototype chain.
This chain of
[[Prototype]]
is the building-block of inheritance in JavaScript.
__proto__
object
To access the object’s [[Prototype]]
, most of the browsers provide a __proto__
property.
This is how we can access it:
// obj is an actual object
obj. __proto__
It’s important to note that, this property is not a part of the ECMAScript standard. It is a de-facto implementation by the browsers.
Get and set prototype methods
Apart from the __proto__
property, there is a standard way to access the [[Prototype]]
.
Here is how we can access the [[Prototype]]
of an object:
Object.getPrototypeOf(obj);
There is a similar method to set the [[Prototype]]
of an object. This is how we do it:
Object.setPrototypeOf(obj, prototype);
[[Prototype]]
and .prototype
property
We have now discussed [[Prototype]]
. It’s nothing but a standard notation to designate the prototype of an object. Many developers get it confused with .prototype property, which is an entirely different thing.
Let’s explore the .prototype
property.
In JavaScript, there are many ways of creating an object. One way is using a constructor function, by calling it using the new
keyword like this:
When you console.log the phone
object, you will see an object with __proto__
property, like this:
Now, if we want to have some methods on the phone object, we can use .prototype
property on the function, as follows:
When we create the phone object again, we would see the following in the console.log
:
We can see the isAndroid()
method in the object’s [[Prototype]]
.
In short, the .prototype
property is basically like a blueprint for the [[Prototype]]
object created by the given constructor function. Anything that you declare in the .prototype
property/object will pop up in object’s [[Prototype]]
.
As a matter of fact, if you compare the SmartPhone.prototype
to the phone’s [[Prototype]]
, you will see that they are the same:
console.log(Object.getPrototypeOf(phone) === SmartPhone.prototype);
// true
It’s worth noting that, we can also create methods inside the constructor function. Instead, we did it using the function’s prototype. There’s a good reason to do so.
Let’s take a look at the following example:
The problem with this approach is when we initiate a new object. All the instances get their own copy of methodA
. On the contrary, when we create it on function’s prototype, all instances of the object share just one copy of the method. Which is more efficient.
What happens when we access a property?
When we access a property either to get it, the following happens:
- The JavaScript engine looks for the property on the object
- If it finds the property, then it returns it
- Otherwise, the JavaScript engine checks the inherited property of an object by looking at
[[Prototype]]
- If the property is found, then it returns it
- Otherwise, it looks into
[[Prototype]]
of[[Prototype]]
. This chain ends when either the property is found or there is no[[Prototype]]
left, which means that we have reached the end of the prototype chain
When we set/create a property, JavaScript always set it on the object itself. Even if the same property exists on the [[Prototype]]
chain. Here is an example:
function MyObject() {}
MyObject.prototype.propA = 10; // creating a property on the prototype
let myObject = new MyObject();
console.log(myObject.propA); // property on the [[Prototype]]
// 10
myObject.propA = 20; // property on the object
console.log(myObject.propA);
// 20
In the above example, we created a constructor function, which has a property propA
on it’s [[Prototype]]
. When we try to access it for the read operation, we see the value in the console. But when we try to set the same property on the object itself; JavaScript creates a new property on the object with the given value. Now if we want to access the property on the [[Prototype]]
directly, we can’t. It’s called the shadowing of property.
It’s also worth noting that the end of a normal object’s [[Prototype]]
chain is built-in Object.prototype
. That’s the reason why most of the object shares many methods like toString()
. Because they are actually defined on Object.prototype
.
Various ways of using prototypical inheritance
In JavaScript, there is just prototypical inheritance. No matter how we create an object. But still, there are subtle differences, that we should take a look at.
Object literal
The easiest way to create an object in JavaScript is by using an object literal. This is how we do it:
let obj = {};
If we log the obj in the browser’s console, we will see the following:
So basically, all the objects created with literal notation inherit properties from Object.prototype
.
It’s also worth noting that __proto__
object has reference to the constructor function, from which it’s created. In this case, the constructor
property points to Object
constructor.
Using the Object constructor
Another, not-so-common way of creating an object is using Object
constructor. JavaScript provides a built-in constructor method named Object
to create Objects.
Here’s how we use it:
let obj = new Object();
This approach results in the same object as object literal notation. It inherits properties from Object.prototype
. Since we use Object
as a constructor function.
Object.create method
With this helper method, we can create an object with another object as it’s [[Prototype]]
like this:
This is one of the simplest ways to use inheritance in JavaScript.
Any guess how we can make an object
without any [[Prototype]]
reference?
Constructor method
Similar to how we have the object constructor function provided by JavaScript runtime. We can also create our own constructor, to create an object which suits our needs as we can see here:
function SmartPhone(os) {
this.os = os;
}
SmartPhone.prototype.isAndroid = function() {
return this.os === 'Android';
};
SmartPhone.prototype.isIOS = function() {
return this.os === 'iOS';
};
Now, we want to create an iPhone class, which should have 'iOS'
as it’s OS. It should also have the faceIDScan
method.
First, we have to create an Iphone
constructor function and inside it, we should call the SmartPhone
constructor, like this:
function Iphone() {
SmartPhone.call(this, 'iOS');
}
This will set the this.os
property to 'iOS'
in the Iphone
constructor function.
The reason why we called SmartPhone.call
method is because we need to change the value of this
to refer to Iphone
. It would be similar to calling the parent’s constructor in an object-oriented world.
The next thing is, we have to inherit methods from SmartPhone
constructor. We can use our Object.create
friend here, as follows:
Iphone.prototype = Object.create(SmartPhone.prototype);
Now we can add methods for Iphone
, using .prototype
as follows:
Iphone.prototype.faceIDScan = function() {};
Finally, we can create an object using Iphone
as follows:
let x = new Iphone();
// calling inherited method
console.log(x.isIOS()):
// true
ES6 class
With the ES6, this whole ordeal is very simple. We can create classes (they are not the same as classes in C++ or other any class-based language, just a syntactical sugar on top of prototypical inheritance) and derive new classes from other classes.
Here is how we create a class in ES6:
class SmartPhone {
constructor(os) {
this.os = os;
}
isAndroid() {
return this.os === 'Android';
}
isIos() {
return this.os === 'iOS';
}
};
Now we can create a new class which is derived from SmartPhone
, like this :
class Iphone extends SmartPhone {
constructor() {
super.call('iOS');
}
faceIDScan() {}
}
Instead of calling SmartPhone.call
, we are calling super.call
. But internally, the JavaScript engine is doing this for us automatically.
Finally, we can create an object using Iphone
as follows:
let x = new Iphone();
x.faceIDScan();
// calling inherited method
console.log(x.isIos()):
// true
This ES6 example is the same as the previous constructor method example. But it’s much cleaner to read and understand.
Conclusion
Let’s summarize what we have learned so far:
- In class-based languages, we can’t run the classes. We have to create objects from them in order to get anything done
- Inheritance in JavaScript isn’t the same as in class-based languages. Because there is no real concept of class. Objects inherit via a reference called as prototype
[[Prototype]]
is just a fancy way of referring to an object’s prototype. They’re both the same thing- We can access an object’s prototype using either
__proto__
property orObject.getPrototypeOf
method - We found out that the function’s prototype property acts as a blueprint for the object’s
[[Prototype]]
which is created using thenew
keyword - We learned what happens when we access a property on an object and what role the prototype chain plays there
- Finally, we also learned about multiple ways of creating an object in JavaScript
I hope this blog post was useful. To learn more about inheritance in JavaScript take a look at the article on MDN.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.