Prototypes in JS
Vaidesh Shankar
Posted on October 24, 2021
Motivation
Lately, I had been reading lots of articles for core concepts of Javascript. After reading a lot of them, I became overwhelmed with so much of knowledge all at once. Though I had been writing keypoints of each concept in docs but I realized it would be better for me to create detailed articles for the concepts that I have been learning.
Now let's start.
Code Example
Let us consider a small program first
function Person(name,age){
this.name = name;
this.age = age;
}
let john = new Person('John',24)
The above program has a constructor Person
and creates its instance john
. Let's log this in browser console
Here we have a [[Prototype]]
property defined apart from the two properties. On opening it further we have constructor
having the value of Person
function and [[Prototype]]
defined once again and further it has a list of properties.
Seeing this we can conclude that there are a bunch of things happening behind as we define just a simple function. We need to understand this for more clarity. For that, let's first understand about Prototype.
What is Prototype ?
According to MDN,
Prototypes are the mechanism by which JavaScript objects inherit features from one another.
By understanding this, definitely there is something related to inheritance. Let's define it a bit more clearly.
Prototype is an object that is available on every function that we define in JS code.
It consists of two things :-
- A
constructor
pointing back to the function on whichprototype
has been defined -
__proto__
(dunder proto) object having the value of prototype of current function's prototype. It same as[[Prototype]]
but we access it using__proto__
. For this article let's use__proto__
in place of[[Prototype]]
These two things would be very important to understand for this article.
Let's use the constructor defined above for depicting the same.
Now let's create two objects for the same.
let john = new Person('John',24);
let james = new Person('James',20);
Logging john
and james
on the console gives us this
Here we can see that both of them have the required set of properties as defined in Person
and have a __proto__
object defined.
On opening it further, we can see that it has Person
constructor and __proto__
object defined. Both of these are same as that of the Person's prototype. Let's check if both are same through referential equality.
console.log(Person.prototype === john.__proto__); // true
Therefore, we conclude that:-
- A constructor's prototype is same as that of its instance. We can access constructor's prototype using
.prototype
while using.__proto__
for that of its instance. - All the instances share the same prototype object of the constructor.
Now that we have understood this much, lets expand the prototype of Person
now.
Here we can seen that Person
has a prototype of Object
which again has a prototype of Object
until null
. This concept of linking prototype object to the parent constructors prototypes all the way up until null
is called as Prototype chaining.
Some observations on prototype properties
NOTE: The examples used in this section are just for understanding purposes and not to be used for creating real world objects.
- Object properties are accessed all the way up through the prototype chain
Let's redefine our constructor in a different way now and create an object for the same.
function Person(){}
Person.prototype.name = "John";
Person.prototype.age = 23;
let john = new Person();
console.log(john); // {}
console.log(john.name); // 'John'
console.log(john.age); // 23
Here we can see that we can access the properties of john
which are not defined in the constructor but defined in its prototype.
This has been achieved through Prototype chaining. All its parent prototypes are searched for the property until we encounter the required property to be accessed. This means that if we had not defined name
and age
in the prototype, all the prototypes of john
have been searched recursively for the property which had not been defined even at the last object up in the prototype chain which would have given us undefined
.
function Person(){}
let john = new Person();
console.log(john); // {}
console.log(john.name); // undefined
console.log(john.age); // undefined
- Setting an object property makes it a direct child even if it has already been defined in its prototype
function Person(){}
Person.prototype.name = "John";
let john = new Person();
console.log(john.name); // 'John'
john.name = 'Carl';
console.log(john.name); // 'Carl'
delete john.name;
console.log(john.name); // 'John'
delete john.name;
console.log(john.name); // 'John'
delete john.__proto__.name;
console.log(john.name); // undefined
Here we can see that on directly setting a property, it becomes a direct child of the object even though it exists in its prototype. Also deletion of property defined in prototype does not happen until we delete it after accessing the object within its prototype.
- Updating a reference type property (through prototype methods) defined in prototype of a constructor modifies it for all its instances
function Person(){}
Person.prototype.friends = ['James','Jaden']
let john = new Person(),
joe = new Person();
console.log(john.fields); // ['James','Jaden']
console.log(joe.fields); // ['James','Jaden']
john.friends.splice(1,0,'Jenny','Joseph');
console.log(john.friends); // ['James','Jenny','Joseph','Jaden'];
console.log(joe.friends); // ['James','Jenny','Joseph','Jaden'];
I hope the example is self-explanatory from the title itself. :D.
By the last two keypoints, we can conclude that
The process of setting or deleting a property is carried out within the property scope while process of getting or updating a property goes within the prototype scopes.
Inheritance using prototypes
Now we will create two constructors out of which the latter will be inherited from the former. Though you might know its class based way of inheritance in ES6 using extends
and super
but lets do it the prototype way now based upon our understanding as of now.
Lets first create the independent constructors
function Person(name,age){
this.name = name;
this.age = age;
}
/**
* Defining function in prototype as it
* is a common functionality shared across
* all the instances
*/
Person.prototype.greet = function(){
console.log(`Hi, I am ${this.name} and my age is ${this.age}.`);
}
function Adult(name,age,occupation){
this.name = name;
this.age = age;
this.occupation = occupation;
}
Adult.prototype.greetAsAdult = function(){
console.log(`Hi, I am ${this.name}, my age is ${this.age} and I work as a ${this.occupation}.`);
}
Here we can see that Adult
can be inherited from Person
constructor considering that we want the greet
function to be a part of the constructor.
How do we do that?
- We can call the
Person
constructor using the scope ofHuman
. We can use.call
method available on prototype ofFunction
constructor. - We need to set the
prototype
ofHuman
to that ofPerson
and constructor to itself.
Firstly, let's do it for Adult
class.
function Adult(name,age,occupation){
Person.call(this,name,age);
this.occupation = occupation;
}
// setting the basic properties of a prototype
Adult.prototype = Object.create(Person.prototype);
Adult.prototype.constructor = Adult;
Using Object.create
, we create an empty object having the prototype properties of Person.prototype
. This is because later we can define our methods on Adult.prototype
without touching the object of base constructor's prototype.
Once we have done this much, now we can define the properties on Adult
's prototype.
Adult.prototype.greetAsAdult = function(){
console.log(`Hi, I am ${this.name}, my age is ${this.age} and I work as a ${this.occupation}.`);
}
Now, let's create an instance of Adult
and call the functions.
let john = new Adult("John",23,"Software Developer");
john.greet(); // Hi, I am John and my age is 23.
john.greetAsAdult(); // Hi, I am John, my age is 23 and I work as a Software Developer.
Conclusion
- Prototypes are objects existing on every function declaration existing within the JS code.
- They form a prototype chain to their parent constructor prototypes until they exist.
- We can declare common functionalities to be shared across all the instances of that constructors within the prototype itself.
- Prototypes play a major role in inheriting properties of base constructors.
End Notes
If you have reached this section, I hope you have gone through the article xD. Feel free to appreciate or criticise the post in the discussions. Show some love if it is useful to you.
Would be writing more posts on Javascript concepts, React concepts and some frontend projects which I am working on nowadays.
References
Posted on October 24, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024