Let's get some terminology aside first.
- Class is a template for what things and functionalities our objects should have.
- Objects are the real existing entities that have properties and functions(methods) attached to them.
- Factory functions, they create new entities as the name suggests. Example: Object.create().
- . notations can be used to grab the property value of an object using a property name.
In JS objects play a very huge role!
Ok, let's dwell deep in.
function userCreator(name, score) {
const newUser = {};
newUser.name = name;
newUser.score = score;
newUser.increment = function() {
newUser.score++;
};
return newUser;
};
const user1 = userCreator("Will", 3);
const user2 = userCreator("Tim", 5);
user1.increment()
This is defenitely not the code you will write for your applications but there are lot of lessons to learn here, let's see what's happening.
- We have created a new factory function userCreator which creates a new object and sets properties for given arguments.
- Here we create two objects, user1, and user2 by invoking the factory function userCreator.
- Think about this, in the last line we invoke the method increment on user1. Now, look at the code of increment in userCreator. We use newUser object which is not present in our global scope, then how can we still invoke increment. That is the magic of closure again!
- Assume, we have 1000 users. For each user, we have a copy of the increment method. Is our code DRY? No.(shortcoming)
- Assume you want to add a property to an object, you manually have to do it. (shortcoming)
Prototype chaining
Store the increment function in just one object and have the interpreter, if it
doesn't find the function on user1, look up to that object to check if it's there.
Link user1 and the object that has functions so the interpreter, on not finding .increment, makes sure to check up in that object where it would find it.
Make the link with Object.create() technique
function userCreator (name, score) {
const newUser = Object.create(userFunctionStore);
newUser.name = name;
newUser.score = score;
return newUser;
};
const userFunctionStore = {
increment: function(){this.score++;},
login: function(){console.log("Logged in");}
};
const user1 = userCreator("Will", 3);
const user2 = userCreator("Tim", 5);
user1.increment();
Object.create(functionStore) automatically references methods in functionStore to our objects.
This way, we are not creating copies of the method increment.
What's going on under the hood? let's see.
- Object.create(userFunctionStore) is going to add reference value to proto property on the object(a hidden property, also read as dunder proto dunder)
- So, when we call user.increment(), we will first look-up if the object has the given property. If we don't find it, JS doesn't panic instead it looks up through the prototype chain to find the methods. This is JS's prototypal nature.
- Hence, we have removed the problem of having copies of methods for each object.
- Note the usage of this keyword in the above code, which generalizes the user on which we are working on. this(the current user) is an implicit parameter which is passed into increment methods.
Inbuilt methods
So, every object has proto and every object is linked to one functionStore by default which is Object.prototype which has some interesting functions like hasOwnProperty. Object.prototype is on the top of the prototype chain(its proto:null).
Some notes:
- Using functions inside methods will have this keyword set to global.
- To overcome this problem, use function.call(this) or use arrow functions which are lexically scoped.
new keyword for cleaner code
When we call the function that returns an object with new in front we automate 2
things
- Create a new user object
- Return the new user object
We need to modify the factory function a little bit to
function userCreator(name, score){
this.name = name;
this.score = score;
}
userCreator.prototype.increment = function(){ this.score++; };
const user1 = new userCreator(“Eva”, 9)
user1.increment()
Look at how we set the object which had all the functions to userCreator function. All functions have a default property prototype which is initialized to an empty object, which is where we store our methods like increment.
Now, every users proto will be set to the userCreator.prototype.
- Understanding all of these concepts is pretty hard for new developers and hence classes were introduced.
- Normally, developers if not using class keyword then they would capitalize these factory functions just to let know the other developers that those factory function would need a new keyword before it.
The class keyword
class UserCreator {
constructor (name, score){
this.name = name;
this.score = score;
}
increment (){ this.score++; }
login (){ console.log("login"); }
}
const user1 = new UserCreator("Eva", 9);
user1.increment();
It does the same thing as the new keyword. But creates a function and object combo. function part has the variable assignments and object part has method definitions.
Let's look at the internals
first, function part, the constructor
function userCreator(name, score){
this.name = name;
this.score = score;
}
and then, sets prototype, remaining code
userCreator.prototype.increment = function(){ this.score++; };
userCreator.prototype.login = function(){ console.log("login"); };
Therefore it is readable, bundled, clean to look at and looks like other languages(but the internals are very different!).
Top comments (0)