Inheritance in JavaScript is not like other famous OOP languages such as Java, C# etc. Coming from Java / C# background to JavaScript , I found it quite confusing at first to understand how this type of inheritance works or how prototypes work in general. This is my attempt to simplify it as much as I can. This will be a long read. There's summary all the way to bottom, if you are just revising the concept.
Basics
Let's start from the very basics, let's create a simple object called bird
and display it's internals using special method provided by Console called dir().
const bird = { flies : true };
console.dir(bird);
Following is printed to console
We can see our flies
property, but there's another property present that we didn't define : [[Prototype]]
. If you try to access this property in code, it won't work, because these properties are hidden or internal to JS engine. We can see that the [[Prototype]]
property is an Object. Let's look inside that object.
We see so much stuff in there. Some of it we probably never use. There are some familiar methods visible like the toString
method or valueof
. All this stuff is available to our bird object through this [[Prototype]]
. That is why when we try to use it like so on bird
,
console.log(bird.toString());
It doesn't throw any errors. Although we didn't define toString
ourselves on bird
, it's available to use because it's present in this hidden property [[Prototype]]
.
Now even if the [[Prototype]]
itself is hidden, we have been provided a special property __proto__
that can be used to tap into it. Let's see it in action.
console.log(bird.__proto__);
prints the same object we saw earlier.
This __proto__
is actually a Getter to read [[Prototype]]
, naturally there's Setter with same name to set value to [[Prototype]]
if we wanted to.
Let's create a parot
object and set it's [[Prototype]]
as our bird
object.
const parot = { speaks : true} ;
parot.__proto__ = bird;
console.dir(parot);
We see something like this ,
It has formed a chain of [[Prototype]]
, which ends all the way back to our original [[Prototype]]
object. Because of this chain we can access flies
property on parot
as well as toString
from the original [[Prototype]]
.
console.log(parot.flies);
console.log(parot.toSting());
If you have property defined with same name as in prototype, you are essentially overriding the property in [[Prototype]]
. When accessing that property, JavaScript will do what is called 'Short circuiting' the chain to return you nearest value.
parot['toString'] = function () {
return JSON.stringify(this);
};
parot.toString();
You no longer see '[object Object]' but you see '{"flies":true}' so it called toString
from parot
itself now.
This chain must end somewhere right? and it does, in the original or you can call it the default [[Prototype]]
. This default [[Prototype]]
comes from Object.prototype
. We will see it in next section.
Object.prototype
It is the default object that is set as [[Prototype]]
when you create an object.
console.log(Object.prototype);
and you will see the same object we saw in birds's [[Prototype]]
.
you will notice [[Prototype]]
is not present on this default object , because it's null
.
To avoid the confusion we are going to get later when we see constructor functions, I will refer to [[Prototype]]
as 'prototype' from now on.
This chain of prototypes is how the inheritance works in JavaScript. Properties of the prototype are inherited by our object. Please note that the prototype can only be set an Object or null, it will not allow any other type to be set as prototype.
__proto__
not recommended
I will not go into this rabbit hole as to why , but the use of __proto__
is no longer considered a good practice, instead we can use methods on Object to do same thing.
1st one sets obj2
as prototype of obj
and 2nd one returns prototype of provided obj
.
Write operations are not affected by prototype
Let's take an example to understand this.
const bird = {
flies: true,
_isFree: true,
set isFree(value) {
this._isFree = value;
},
get isFree() {
return this._isFree;
},
};
const parot = { speaks: true };
Object.setPrototypeOf(parot, bird);
parot.isFree = false;
console.log(parot.isFree);
console.log(bird.isFree);
First we defined a bird object with getter and setter methods to set value of property _isFree
.Then we created parot object and set bird as prototype of parot. We proceed to calling isFree
the setter, on parot and set the value to false
.
Calling the setter isFree
on parot is a write operation and naturally the question appears in mind, will it change the _isFree
property of the bird object ? because the setter method is defined on bird and not on parot.
The answer is , it doesn't. Instead it creates _isFree
property on parot and sets it's value to false
. _isFree
property on bird is not affected.
Technically what this means is, value of this
is not affected by prototype. The value of this
when calling isFree on parot, is the parot object. It behaves as expected, value of this
is taken from calling context.
Constructor Functions
In earlier section we saw how prototypical inheritance works but how do we actually put it to some practical use? We saw in the last example where we set some getter setters on bird that were reusable on parot but is there a way were we can make this generic to create objects with some blueprint or structure and add methods that are reusable? The answer is yes, with the help of Constructor functions!
Here's how we create a constructor function,
function Bird(flies, isFree) {
this.flies = flies;
this._isFree = isFree;
}
const parot = new Bird(true, false);
a constructor function is just like your normal function that accepts arguments and set's the argument values to this
.To create an object using constructor function we call the function with new
keyword. Here's where things get little complicated. It sets prototype i.e [[Prototype]]
of the object you create to an object that exists in prototype
property on it.
Let's demystify that last statement. Each constructor function or rather any function in JavaScript has a prototype
property on it.
console.log(Bird.prototype);
you can see there's a property called constructor
which contains the reference to itself i.e Bird function and [[Prototype]]
.
We can easily prove that above object is set as [[Prototype]]
for newly created parot,
console.log(Object.getPrototypeOf(parot));
console.log(Object.getPrototypeOf(parot) === Bird.prototype);
This means we can define properties on Bird.prototype
that will be accessible to parot. Let's do that and change code code like so,
function Bird(flies, name) {
this.flies = flies;
this.name = name;
}
Bird.prototype.flying = function () {
console.log(`${this.name} is flying`);
};
const parot = new Bird(true, 'parot');
parot.flying();
and you will see 'parot is flying'. This is how we can put the prototypical inheritance to some use.
By the way did you realize something? recall the Object.prototype
we saw way before at the start. Yes, Object itself is a constructor function!
Native Prototypes
Now that we have better understanding of constructor functions and [[Prototype]]
. Let's discuss what are native prototypes.
const user = {name: "hrishi", age: 26};
console.log(Object.getPrototypeOf(user));
and you shall see the default [[Prototype]]
value we have been seeing. It is taken from Object.prototype
. Internally when you use {...}
syntax is it calling new Object()
and assigning the value in Object.prototype to [[Prototype]]
of your new object. We can easily prove that.
console.log(Object.getPrototypeOf(user) === Object.prototype);
Other build-in types like Array
, String
, Number
follow the same thing. They are constructor functions with some methods on them that can be reused on instances you create.
for example,
startsWith
, toUpperCase
methods on strings are defined on String.prototype
.
map
, filter
, includes
methods on the arrays are actually defined on Array.prototype
and so on.
Summary or TL;DR
- All the objects in JavaScript have a hidden property
[[Prototype]]
which itself is another object or null. - This property can be accessed or set using
__proto__
getter and setter methods available in[[Prototype]]
. - It is no longer considered a good practice to use
__proto__
, ratherObject.setPrototypeOf
andObject.getPrototypeOf
should be used. - If a property does not exist on the object, JavaScript looks for it in the prototype chain and fetches it wherever it is available nearest to the object.
-
this
and write operations are not affected by prototypes. - Constructor functions allows us to create objects using
new
syntax that sets[[Prototype]]
of newly created object to an object in Constructor.prototype. This only happens when function is called withnew
otherwise there's no such effect. - All built in types such as
Array
,String
,Number
,Date
... are constructor functions that define methods usable on objects we create using them. - Object itself is a constructor function , all other built in constructor functions have their
[[Prototype]]
= Object.prototype.
I hope this helped you understand prototypes better. I will let you explore from here on out.
Top comments (0)