Classes and Prototypes?
It sounds kind of cool!
Every coder has heard about classes, but I am not sure many developers have a deep understanding of prototypes.
Today, we will talk about classes and prototypes and we will get a better idea of how they work.
Also, this post is the last of the How Js works series, it took me a while, but with this post, the series is finalized yay!
The purpose of this post is to learn how we can organize our data in different ways.
We can start with the a basic example:
1: const banana = {
2: name: 'banana',
3: amount: '2',
4: }
Imagine we want to reduce the number of bananas, how can we do it?
An easy way that follows the principe of encapsulation will be to add the function directly on the object!
On the banana object, we add a property called decrease
, and when we called it, the banana's amount will decrease by one.
1: const banana = {
2: name: 'banana',
3: amount: '2',
4: decrease: () => banada.amount--;
5: }
6: banana.decrease()
Another way to achieve this will be using the dot notation to create the same object.
1: const apple = {};
2: apple.name: 'apple';
3: apple.amount: '2';
4: apple.decrease: () => apple.amount--;
And there are even more ways to do this.
We can also use Object.create
1: const pinneaple = Object.create(null);
2: pinneaple.name: 'pinneaple';
3: pinneaple.amount: '2';
4: pinneaple.decrease: () => pinneaple.amount--;
Ok.
There are many ways to do it, but as you can see, for every piece of fruit, we also need to create every property, and it gets repetitive quickly, we are breaking the DRY principle.
Let's try to create fruits following the DRY principle.
We can create a function that will create an object for our fruity needs!
Welcome The fruitCreator™.
1: function fruitCreator(name, amount){
2: const newFruit = {};
3: newFruit.name = name;
4: newFruit.amount = amount;
5: newFruit.decrease = function() { newFruit.amount-- };
6: return newFruit;
7: };
8: const orange = fruitCreator('orange', 2);
9: const cherry = fruitCreator('cherry', 1);
Do you see any problem with this method?
There are two big ones:
So what would be a better way to create objects without having to store the same function on each object?
With the Prototype Chain, we can store the function on an object and have the interpreter look up to that object if the function is not found on the object created.
What?
Ok, let’s take a look...
The link we are talking about can be done by using the Object.create()
technique, let's check it out.
1: function fruitCreator(name, amount){
2: const newFruit = Object.create(fruitFunctionStore);
3: newFruit.name = name;
4: newFruit.amount = amount;
6: return newFruit;
7: };
8:
9: const fruitFunctionStore = { decrease: function(){ this.amount-- } };
10: const orange = fruitCreator('orange', 2);
11: const cherry = fruitCreator('cherry', 1);
12: orange.decrease();
We create an object called fruitFunctionStore, on this object we assign a function to the variable decrease.
When on line 2, we create a new object with Object.create
, even tho we are passing the function fruitFunctionStore
,
the object created will still be empty.
On line 12, when we call decrease
, there is no reference to the decrease function on the local context of the orange object, so where does the execution context find it?
Since we pass the fruitFunctionStore
object when using Object.create
, a prototypal link has been created, this means that on the orange
object, under the __proto__
property, you will find a link to the fruitFunctionStore
object, where the execution context will find the decrease
function.
Now any fruit created has, thanks to the __proto__
hidden property, a prototypal link to fruitFunctionStore
. Only once fruitFunctionStore
needs to be created, solving the issue we had before where we were storing the same function on every object!
When using Object.create()
the parameter passed will be store on the __proto__
property of the object created.
Nice!
We should mention that this
on this.amount
, is an implicit parameter, when executing line 12, on the local memory of the execution context of decrease
, the identifier this
will automatically be assigned the value to the left of the function called, in this case, orange
, so it becomes, orange.amount--
.
What if there is even an easier way to achieve this.
Let´s take a look
The superpowers of the new keyword (it automates a lot of the work for us):
1: function fruitCreator(name, amount){
2: this.name = name;
3: this.amount = amount;
4: }
5: fuitCreator.prototype.decrease = function(){ this.amount-- };
9: const orange = new fruitCreator('orange', 2);
To understand this, we need to clarify something, JS functions are both objects and functions, so we can store inside the prototype
property our function fruitCreator
.
When using the new keyword, the new object created orange
will have on the __proto__
property a prototypal link to the fuitCreator.prototype
.
This under the hood works in the same way as Object.create(fruitFunctionStore)
, but the new keywords do a lot of the work for us!
Every object on JS has a link on the __proto__
value to the Object.prototype
object, where there are functionalities stored like hasOwnProperty
, useful to check if an object has a determined property.
When using new
, the new object created will be directly assigned to this
.
Well, this is the end of the How JS works series. I hope you enjoyed as much as I did!
Also, I honestly can't recommend enough Javacript the Hard Parts 2 by Will Sentance. If you are serious about learning JS, it is a must.
See ya!