Prototype in JavaScript

Prototype in JavaScript

Understanding Prototypal Behavior :

When you write the following code in the browser console :

const newHero = ["hulk", "spiderman"];

Pressing enter gives undefined because we haven't returned anything; we've simply declared an array and stored it in memory.

If you then type newHero and press enter, you will see:

> newHero
(2) ['hulk', 'spiderman']
0: "Hulk"
1: "spiderman"
length: 2
[[Prototype]]: Array(0)

This output highlights JavaScript's default prototypal behavior. Every object in JavaScript has a prototype. A prototype is an object that acts as a template from which other objects inherit properties and methods.

Prototypal behavior means that if we ask JavaScript to find a property or method, it will check every layer available, accessing parents, grandparents, and beyond until it either finds the value or returns null.

Example of Prototypal Behavior

let animal = {
  eats: true
};

The animal object has a prototype from which it inherits properties and methods. If you try to access a property that doesn't exist on animal, JavaScript will look for it in the prototype.

If the property isn't found in the object's prototype, JavaScript will continue to look up the chain of prototypes until it either finds the property or reaches the end of the chain (null).

Key Point :

Because of prototypes, we can access the new keyword, which also gives us access to "classes" and "this".

Everything in JavaScript is an Object

When we declare any object like a String, Array, etc., in JavaScript, they are actually objects:

Array ---> object ---> null
String ---> object ---> null

There is no parent of Object. The properties and methods inside an object belong to the object, thus we get a reference to null.

Functions are Objects Too

function multiplyBy5(num) {
  return num * 5;
}

We can label functions and use dot notation with them, indicating that functions are also objects. For example:

multiplyBy5.power = 2;
console.log(multiplyBy5(5)); // 25
console.log(multiplyBy5.power); // 2
console.log(multiplyBy5.prototype); // {}

In the end, we can say that everything inside JS is an object and the properties that are available in the object, those properties are also available to strings and arrays.

Adding Methods to Functions Using Prototypes

So as we know, functions are also objects, so can we inject functionalities inside the functions?

So can we introduce our functionalities inside the function, as functions are also objects, and inside the object there are properties and we can create a property that can hold our function.

We can inject functionalities into functions. For example:

createUser.prototype.increment = function(){
    score++;
}

createUser.prototype.printMe = function(){
    console.log(`score is ${score}`);
}

const chai = createUser("chai", 25);
const chai = createUser("tea", 250);

Now, through "prototype" we have injected the "increment", but the problem is we don't know "which one we have to increase score, chai or tea".

It doesn't have an idea or context of which value it has to increment, or we can say it don't know that either chai has called the function or tea has called the function.

Solution : use "this" keyword (Using this ensures that the function operates on the correct instance. We can add more methods to createUser:)

createUser.prototype.increment = function(){
    this.score++;
}

createUser.prototype.printMe = function(){
    console.log(`score is ${this.score}`);
}

const chai = createUser("chai", 25);
const chai = createUser("tea", 250);

When the function is called, we don't have to write like this "chai.prototype.printMe()", instead we can directly write "chai.printMe()".

But, while calling the function, it will throw us an error that says "can not read properties of undefined", so when we transferred the value from the function to the variable like "const chai = createUser("chai", 25)", we didn't told chai about its additional properties like printMe and increment.

So all this work of telling about new functionalities is done by the "new" keyword, we have to write like this "const chai = new createUser("chai", 25)".

createUser.prototype.increment = function(){
    this.score++;
}

createUser.prototype.printMe = function(){
    console.log(`score is ${this.score}`);
}

const chai = new createUser("chai", 25);
const chai = new createUser("tea", 250);

The Significance of the new Keyword

When we use the new keyword, several things happen behind the scenes:

  1. A new object is created: The new keyword initiates the creation of a new JavaScript object.

  2. A prototype is linked: The new object is linked to the prototype property of the constructor function, giving it access to properties and methods defined on the constructor function.

  3. The constructor is called: The constructor function is called with the specified arguments, and this is bound to the new object. If no explicit return value is specified, JavaScript assumes the new object to be the intended return value.

  4. The new object is returned: After the constructor function has been called, the new object is returned unless the constructor explicitly returns a non-primitive value.

Prototypes

In JavaScript, the concept of prototypes is fundamental. It enables objects to inherit properties and methods from other objects, forming a chain of inheritance known as the prototype chain. Let's dive into this concept with some practical examples and see how it impacts our code.

Basic Example: Prototype Chain

Consider the following example where we declare a string and check its length:

let myName = "harsh";
console.log(myName.length); // outputs: 5

If we add spaces to the string, the length property reflects those changes:

let myName = "harsh     ";
console.log(myName.length); // outputs: 10

The length property counts all characters, including spaces. Suppose we want a method that tells us the true length of the name without spaces. We could use trim().length every time, but let's add a method called trueLength to the String prototype instead.

Adding trueLength Method

Now, let's create a method called trueLength that will tell the true length of the string, excluding spaces.

let myName = "harsh     ";
console.log(myName.trueLength());  // but if we run this, it will say undefined, because trueLength is not a defined method

We could use console.log(myName.trim().length) every time, but instead, let's add a method to the String prototype.

//before reading this code, first read and understand rest of the
//code and come here at last

String.prototype.trueLength = function() {
  console.log(`${this}`);
  console.log(`True length is: ${this.trim().length}`);
};

let anotherUsername = "harsh     ";
anotherUsername.trueLength(); 
// outputs:
// harsh
// True length is: 5

"harsh".trueLength(); 
// outputs:
// harsh
// True length is: 5

Now, any string instance can use the trueLength method.

Exploring Prototypes with Arrays and Objects

Let's declare an array and an object:

let myHeroes = ["thor", "spiderman"];
let heroPower = {
  thor: "hammer",
  spiderman: "sling",
  getSpiderPower: function() {
    console.log(`Spidy power is ${this.spiderman}`);
  }
};

Adding Methods to Prototypes

We can inject a function into object like this "heroPower.prototype.functionName = function(){----}", but we will follow a different approach here

We can inject a method into an object prototype directly: if any object is declared, then directly declare inside that object, as we know "function --> object --> null", "array --> object --> null", "string --> object --> null"

Object.prototype.javascript = function() {
  console.log("JavaScript is present in all objects");
};

heroPower.javascript(); // outputs: "JavaScript is present in all objects"
myHeroes.javascript();  // outputs: "JavaScript is present in all objects"

So here we introduced a function inside object then it will be available in all the things that are objects (function, array, string), and all will have that method.

Adding Methods to Specific Prototypes

If we add a method specifically to the Array prototype, it won't be available to objects that are not arrays:

Array.prototype.heyJavascript = function() {
  console.log("JS says hello");
};

myHeroes.heyJavascript(); // outputs: "JS says hello"
heroPower.heyJavascript(); // throws an error

Prototypal Inheritance

Let's explore inheritance using prototypes:

const Teacher = {
  makeVideo: true
};

const User = {
  name: "tea",
  email: "tea@google.com"
};

const teachingSupport = {
  isAvailable: false
};

const TASupport = {
  makeAssignment: "JS Assignment",
  fullTime: true
};

Now every object is a different instance on its own, its not like they are sharing some details with each other, each object has different properties, default properties of every object is the same like length, etc.

In some situation, we want to exchange some information, that we want to say "link these 2 objects", and for this linking purpose, we know that we have "prototype".

So we need to have a property inside objects to link other with it, namely "__proto__"

Linking Objects with __proto__

We can link objects using the __proto__ property:

const TASupport = {
  makeAssignment: "JS assignment",
  fullTime: true,
  __proto__: teachingSupport
};

console.log(TASupport.isAvailable); // outputs: false

Now the new objects we will create using "new" keyword (const TAS = new TASupport), then we will have access to the properties of TeachingSupport inside that new object.

Also we can write this outside the object like

Taecher.__proto__ = User
//Now teacher can also access the properties of User

New syntax for linking objects

We can also set the prototype using Object.setPrototypeOf:

Object.setPrototypeOf(teachingSupport, Teacher);
console.log(teachingSupport.makeVideo); // outputs: true

Now teachingSupport can access all thee properties of Teacher.

New instances of teachingSupport will have access to Teacher properties:

const newTA = Object.create(teachingSupport);
console.log(newTA.makeVideo); // outputs: true

This concept is known as prototypal inheritance.

Conclusion

Understanding prototypes in JavaScript allows us to create more efficient and powerful code by leveraging inheritance and extending object functionalities. By adding methods to prototypes, we can create reusable and consistent behavior across different instances of objects, arrays, and functions. This helps in writing clean, maintainable, and modular code.