Hey ๐ this will be a bit long , but detailed post where we will try to understand using an example.
Assume we have to build a game where users can register with username against which score would be updated. So, the below code would do the job right?
function generateUser(name,score){
let newUser = {};
newUser.name = name;
newUser.score = score;
return newUser;
}
When ever we need to create a new User, we can use generateUser("<User entered name>" , 0)
which would return us the new user Object.
So, the next thing we need to take into consideration is to increment the score if the user wins the game.
Obviously, the immediate choice would be to create a new function as below :
function incrementScore(user){
user.score++;
console.log(user);
}
and whenever we need to increment the score, we can just use this function incrementScore(user1);
But the problem here is that, when our code is modular/grows into huge codebase in future, we might not know where the increment function is located and also, in a case where we might need other operations like changing the name or adding a new property , we cannot just keep creating new functions as that would clutter the codebase and would become difficult to maintain.
So , how do we solve that?
Well, in 1 word โ prototype.
But lets slowly try to solve this problem by understanding each step.
What if we could just put the functions inside out generateUser as below?
function generateUser(name,score){
let newUser = {};
newUser.name = name;
newUser.score = score;
newUser.incrementScore = function() {
newUser.score++;
}
return newUser;
}
By this we can achieve the below :
let user1 = generateUser('BruceWayne',10);
user1.incrementScore();
Perfect! , now we can just use the user Object itself to incrementScore or change name or whatever...
But, let's consider there are 200 users, in which case it is really painful/in-efficient memory usage to save the incrementScore
function for every user when we know all it does is incrementScore by 1 and this operation is same across all users.
Turns out we can further optimize this approach using Object.create()
method as below :
function generateUser(name,score){
let newUser = Object.create(userFunctionsStore);
newUser.name = name;
newUser.score = score;
return newUser;
}
let userFunctionsStore = {
increment: function() { this.score++ ;}
}
let user1 = generateUser('BruceWayne',10);
console.log(user1); // { name: 'BruceWayne', score: 10 }
user1.increment();
console.log(user1); // { name: 'BruceWayne', score: 11 }
Ohkay! , Bunch of stuff there..
We have now modified the generateUser()
to create a new Object using Object.create()
method instead of {}
, using which we can now achieve classical inheritance.
More information here do check it out.
So, Object.create(userFunctionsStore)
means, any and all functions declared inside of userFunctionsStore
will be accessible by all newUser
Objects. this is possible because the functions inside userFunctionsStore
are present in the Object instance of Object.create
in the prototype property which is present in the global memory space, which is referred by any new newUser
Objects using a link in _proto_ property implicitly.
Using this property explicitly is deprecated , more info here
Now, the code is looking little better and also more maintainable.
But , there still is a bunch of code that we can avoid, As currently we are creating the prototype bond using Object.create()
using a userFunctionsStore
Object , but we can automate all of that using a keyword new
as below :
function generateUser(name,score){
this.name = name;
this.score = score;
}
generateUser.prototype.increment = function() {this.score++};
let user1 = new generateUser('Bruce Wayne',10);
console.log(user1); // { name: 'Bruce Wayne', score: 10 }
user1.increment();
console.log(user1); // { name: 'Bruce Wayne', score: 11 }
We are able to add functions to the prototype of the generateUser
function explicitly and also we need not create , call , return the object from generateUser
. And All user Object will be able to access the prototype functions by utilizing prototypal inheritance.
It's amazing how much stuff the new
keyword does for us. Read more about it here
The code now seems perfect. But there are still some changes we can do to make the code more elegant, Since currently to call generateUser()
the new
keyword is needed, without which the this
keyword would point to Global this
.
To solve this we can use a new syntax called class
.
Also the best practice is to capitalize the first letter of function when we need to use new
keyword for calling the function , in this case :
function GenerateUser(name,score){
this.name = name;
this.score = score;
}
Bonus :- class in JS
Ohkay! , Now we will try to use class
to replace the function
as below :
class GenerateUser {
}
Now we need a function, which to assign the name and score , which we can do in the constructor which is called when we call the class.
class GenerateUser{
constructor(name,score){
this.name = name;
this.score = score;
}
}
let user1 = new GenerateUser('Bruce Wayne' , 10);
console.log(user1); //{ name: 'Bruce Wayne', score: 10 }
As simple as that, looks more clean.
But now we need to make an increment()
function which we can directly declare inside a class as below :
class GenerateUser{
constructor(name,score){
this.name = name;
this.score = score;
}
increment() {
this.score ++;
}
}
let user1 = new GenerateUser('Bruce Wayne' , 10);
console.log(user1); //{ name: 'Bruce Wayne', score: 10 }
user1.increment();
console.log(user1); //{ name: 'Bruce Wayne', score: 11 }
Nothing has changed by using class
instead of function
, all the underlying principles are same, as we saw before using function prototypes. Just that the code is more readable and maintainable. And now you know how it works under the hood.
Thanks to Will Sentance and Kyle Simpson for their amazing work.
Let me know if you have any doubts or any issues!.
Thank you ๐ !
Top comments (0)