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:
A new object is created: The
new
keyword initiates the creation of a new JavaScript object.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.
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.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.