JavaScript introduced classes in 2015, which many developers believe to be just syntactic sugar over the prototype model of JavaScript (which is kinda true, but there are some differences, and it’s not all the same under the hood).
It’s a bit confusing for people who are just starting to learn JavaScript, as they will come across two approaches to using object-oriented programming: the Prototype model and classes.
Let’s try to understand it.
Historically, we had the Prototype-based model of OOPS in JavaScript (It’s still there, but now we also have classes). The Prototype-based model is not specific to JavaScript but is used in some other languages too. It is a way to implement OOPS; objects are created without classes, and objects can inherit from other objects to reuse existing functionality. This is also known as classless or instance-based programming. JavaScript is the only mainstream language that uses this model.
Because of this approach, almost everything in JavaScript is an object except for primitive data types (for primitives also, there are object wrappers for providing them access to native object methods like toString()). This is because everything is built on top of the base Object prototype in JavaScript. I know this might be confusing, but we will understand this later in the article.
There are all sorts of opinions regarding classes in JavaScript as some people like it, some don’t, and some just like some parts of it only. This is because classes in JavaScript are based on the Prototype-based model only under the hood and not so much different, even though there are some differences. But the implementation is pretty similar, so you can convert any class in JavaScript into the prototype-based implementation. This is a whole another discussion we can save for later; let's focus on the topic for now.
Let’s understand some basics before. To understand the topic, we need to understand the following concepts:
Prototypal Inheritance and Prototype chain
Constructor functions to understand function prototype.
As I have discussed this before and you might also know that in JavaScript, we don’t need classes to create objects; we can just define them like any variable in JavaScript. I have also mentioned that in the Prototype-based model, objects can reuse existing properties and methods from other objects to avoid code duplication and save memory. This is nothing but inheritance only and known as Prototypal Inheritance. When objects share their properties and methods with other objects, it’s called a Prototype for those objects. Confusing? Let's understand it with some examples.
Let's create a user object array. This is how we usually deal with multiple objects. Each object has a first name and last name as properties.
const Users = [
{
firstName: "Nitesh",
lastName: "Khatri",
},
{
firstName: "Arun",
lastName: "Verrma",
},
];
At some point, you have this requirement that you also want to have a full name for every user. You have a few solutions for this problem; some require inheritance, and some don’t. For example, you can just manually update it, which is really tedious and time-consuming and often not a viable solution. You might say we can use a loop to add a full name to each object, something like the following.
const UsersData = Users.map(user => ({
...user,
fullName: `${this.firstName} ${this.lastName}`;
}));
This will work, but the problem is you are storing an extra property which can just be derived from object properties. And suppose if the array contains thousands of user objects, you are just increasing the memory space by adding an extra property. In this case, it is one property only, but for some other use cases, it can be multiple properties and methods also.
Okay, so you might say, now we can just use the properties directly like ${user.firstName} ${this.lastName}. Yes, we can, and it will work fine. But let's say the full name is used in a lot of places, and you want to have a shorter version where you can just write ${user.fullName}, that's it. For this, we can use an object method or, even better, a getter/setter.
You might be tempted to add a fullName() {} method to the objects, which returns the full name, but this will create the same problem I have just mentioned above that we are adding a property or method to each object that takes extra space.
So what’s the solution?
Well, we can use prototype inheritance to store this method once, and then it can be used by all the objects that inherit it. But how do we do that? By adding a userPrototype from which all other objects inherit the fullName() method.
const userPrototype = {
function fullName() {
return `${this.firstName} ${this.lastName}`
},
}
const Users = [
{
firstName: "Nitesh",
lastName: "Khatri",
},
{
firstName: "Arun",
lastName: "Verrma",
},
];
const UsersData = Users.map(user => ({
...user,
__proto__: userPrototype,
}));
console.log(UsersData[0].fullName);
You might say, Wait a minute! I told you all not to add an extra property to save memory, but I have done the same thing, and what is this proto thing and why it’s added.
Okay, let's understand the above code. First of all, the proto property is a special property that stores a reference to an object. Every object in JavaScript has this hidden property [[Prototype]] which points to an object from which it inherits. And by default, every object inherits the native Object.prototype. As I mentioned, the prototype model is based on objects being able to reuse existing functionality by inheriting from each other, but for that, we need a reference which [[Prototype]] property gives us.
proto is an accessor property. In simple words, it gets or sets the hidden [[Prototype]] property for us. So adding this property does not increase the size of the object. First, it already exists on every object, and second, it only stores the reference, not the object itself. So, no matter how big the prototype object is, the original object's size remains the same. So when we set proto to userPrototype, it just means that userPrototype object is now the prototype for the user objects, and the user objects can inherit its properties and methods.
I don’t know whether you have ever noticed or not, but you can check this in the console. Every object has this hidden property [[Prototype]], which will be pointing to some other object. in the following case it points to Object.prototype which is default.
In our case, you can see that this property points to the userPrototype object, which has the fullName() method. And it does not stop there. You can see that the userPrototype object also has [[Prototype]] property, which further has some methods. This is the Object.prototype from which every object inherits, as mentioned earlier, and it is set by default. So when a property is not found, it goes to the prototype of that object and checks there. If not found there also, it goes to the prototype of the prototype and checks there. And at the very end is the Object.prototype, which has no further prototype, so it points to null, and undefined is returned for a object property. This is know as the prototype chain.
In the most simple words, if we have two or more objects and we want to share properties or methods between them to save memory, we can just link them using prototypal inheritance by giving the reference of the prototype object.
There are multiple ways to set this reference. One way is using proto, which you have already seen, but there are some do’s and don’ts for the same.
const obj = { __proto__: userPrototype } //-> recommended
obj.__proto__ = userPrototype //-> Possible but not recommended
obj = Object.create(userPrototype) // -> recommended when setting outside the obj
Object.setPrototypeOf(obj,userPrototype)
// Above can also be used but to be cautious about the performance as when
// set during runtine Object.setPrototypeOf takes time to execute
You can use proto to set the reference inside the object. For example: const obj = { proto: someObjReference } and not obj.__proto__ = someObj.
Although the latter one will also work, it is not recommended. If you want to set the prototype outside the object, you can use Object.create(obj, [descriptors]) or Object.setPrototypeOf(obj1, obj2). And to get the prototype, you can use Object.getPrototypeOf(obj), same as obj.__proto__ but not recommended.
Good to know: A few years back, the [[Prototype]] hidden property was named as proto only. Historically it has always been proto, but later changed to [[Prototype]], and proto is now used as the accessor property.
Now let’s come back to the problem again. We have now added a fullName() method that is inherited in all the user objects and takes no extra space. If we want, we can stop here, but we can also extend the current functionality by using getter and setters function in JavaScript. Take a look at the updated code.
const userPrototype = {
get fullName() {
return `${this.firstName} ${this.lastName}`
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(" ");
}
}
const Users = [
{
firstName: "Nitesh",
lastName: "Khatri",
},
{
firstName: "Arun",
lastName: "Verrma",
},
];
const UsersData = Users.map(user => ({
...user,
__proto__: userPrototype,
}));
console.log("Full name of user is: ",UsersData[0].fullName);
UsersData[0].fullName = "Advitya sharma"
console.log("Full name of user is: ",UsersData[0].fullName);
Result of the following code
Full name of user is: Nitesh Khatri
Full name of user is: Advitya sharma
What I have done is added a getter and setter function which gives us the value of the full name when we call user.fullName. From outside the object, it is just like a normal property, and that’s the point of a getter and setter function; from outside, it just looks like a normal property. But within the object, we have a get and set function which defines the behavior on getting and setting the property. We could have used methods like getFullName() and setFullName() to do the same job, but I feel the getter and setter functions are more suited for such situations. It just takes away a layer of implementation and gives you the abstracted version.
Okay, so I think you have a fairly good picture of what is the prototype model and prototypal inheritance. We can move forward to constructor function.
As I have mentioned above that prototype-based OOPS not necessarily have classes, which means that we can define objects without classes. Now the problems that classes solve for sure is that first it gives you a blueprint for an object, and second, you can create as many objects with that blueprint with different values. In the case of JavaScript, how do we do this? Now we can use classes, but what about the time when JavaScript did not have classes?
Well, the answer is Constructor Function.
In JavaScript, we have constructor functions which are kind of like classes (if that is easy for you to understand this), providing a blueprint to create objects and called with the new keyword.
function User(fname, age) {
this.name= name;
this.age = age;
}
const user1 = new User("Nitesh Khatri", 23);
const user2 = new User("Arun Kumar", 22);
How it works is when we call a function with the new keyword, it is treated as a constructor function. JavaScript creates an empty object and passes it to the function. Inside the function, we can define our properties that we want the object to have. Note that properties are defined using "this.propertyName", because inside the function "this" stores the references to the object that is created and passed to the function and later the function will automatically return an object with all these properties added to it. If we want, we can also return a custom object from the constructor function if that is the requirement. Do remember that we can only return an object; any other value will be ignored.
So what const user1 = new User("Nitesh Khatri", 23); does under the hood is to first create an empty object as the function is called with new. And inside the function, all the properties are initialized with the values being passed to the function, and later it returns an object with properties and methods that are added.
This just feels like a class kinda thing, at least to me it does. Might be my personal opinion.
Okay, now what about the inheritance? We discussed that objects can inherit from other objects. How can we do the same with a constructor function?
All constructor functions have a special property “prototype,” just like all objects have a special property “__proto__”. And as you might have guessed, it is used to set the prototype for objects that we create using a constructor function.
By default, this f.prototype property points to an object that has a single property constructor which points back to the constructor function itself. Confusing, right? I know. You can skip it if you find it hard to understand. This is because when we create an object using a constructor function, it will have that hidden [[Prototype]] property which will point to this object I mentioned above by default. In most cases, we don’t require it. In cases when you are using external code and not aware of which constructor function a object is created with or you want to create another object with that constructor function but it is available, you can use this to do the same.
You can see in the below code that using user1 we can get the constructor function. As by default, we know that User.prototype points to an object with a constructor property which points to User itself. So now we have User inside UserConstructorFunction, and we can use it to create new users.
function User(fname, lname, age) {
this.fname = fname;
this.lname = lname;
this.age = age;
}
const user1 = new User("Nitesh", "Khatri", 23);
const UserConstructorFunction = Object.getPrototypeOf(user1).constructor;
const user2 = new UserConstructorFunction("Arun kumar", 24)
In some situations, we just have access to the user1 object and we are not aware of how it’s created, so we can use this way if this is what is required. It’s a pretty advanced use case.
To understand function prototype and inheritance using constructor functions, let's add the same getter and setter function to objects for getting the full name. We have to modify the function a little bit, and the result will be the following.
function User(fname, lname, age) {
this.fname = fname;
this.lname = lname;
this.age = age;
}
User.prototype = {
constructor: User,
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(" ");
},
};
const user1 = new User("Nitesh", "Khatri", 23);
console.log(user1.fullName) // -> Nitesh Khatri
I have added the getter and setter function to User.prototype, and you will also notice I kept the constructor: User property, which is the same thing as I explained above. As we are assigning a new object, the default will be removed, so I am adding it here also. Not necessary but good to have.
I hope you have learned something new today. I have not discussed classes today, I might in some future article but for now I think this is enough. There is a lot that I haven’t covered like some more advance use cases related to prototype inheritance, getter/setter and constructor function but it will then keep on going and this article will become a mini book itself. This article covers the things that you are required to understand about prototypal inheritance and constructor function, you always have the option to extend your knowledge by reading more about them.
Join Nitesh on Peerlist!
Join amazing folks like Nitesh and thousands of other people in tech.
Create ProfileJoin with Nitesh’s personal invite link.
1
3
0