Prototypes in JS

vaideshshank

Vaidesh Shankar

Posted on October 24, 2021

Prototypes in JS

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)
Enter fullscreen mode Exit fullscreen mode

The above program has a constructor Person and creates its instance john. Let's log this in browser console

image

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 which prototype 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.

Constructor and prototype

Now let's create two objects for the same.

let john = new Person('John',24);
let james = new Person('James',20);
Enter fullscreen mode Exit fullscreen mode

Logging john and james on the console gives us this
Loggin john
Logging james

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
Enter fullscreen mode Exit fullscreen mode

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.

Constructor with its instances and prototype

Now that we have understood this much, lets expand the prototype of Person now.
EXpanding Person prototype

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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'];
Enter fullscreen mode Exit fullscreen mode

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}.`);
}
Enter fullscreen mode Exit fullscreen mode

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 of Human. We can use .call method available on prototype of Function constructor.
  • We need to set the prototype of Human to that of Person 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;
Enter fullscreen mode Exit fullscreen mode

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}.`);
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
vaideshshank
Vaidesh Shankar

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