Hello everyone π, this article is also part of my series Javascript: The Hard Parts v2 Cover. Though you don't have to read the previous articles to follow up with this article. But I recommend reading my previous articles in the series π.
Introduction
There are 4 different ways of storing data (properties) and functionalities (methods) together in one place which can be used multiple times as need. In this article, we'll go over the 4 different ways and get to a standard way at least for now to achieve the same goal.
Firstly, why do we need objects, have you ever thought of that?
Well, here's a scenario. Let's say you'll like to store some data that belongs to a particular user in your application, probably a profile application. This user has to perform some instructions (functionality) and you want both the functionality and the data of that user to be used by only to that user.
From the scenario, that's where Objects comes into play right!
Example 1:
const user1 = {
name: "John",
dob: 1999,
age: function () {
let today = new Date().getFullYear();
return today - user1.dob;
},
};
From the example above, we have some data (properties) which are name
and dob
(date of bate) and a functionality (method) age
which belongs to that specific user. But what if we'll like to have another user with similar data. Another similar object will be created.
Example 2:
const user2 = {
name: "Sally",
dob: 2002,
age: function () {
let today = new Date().getFullYear();
return today - user2.dob;
},
};
Now, we have two users user1
, user2
having their data (property) and functionality (method) specific to both of them, but this introduces repetition breaking the DRY - Don't Repeat Yourself principle.
Since both user1
and user2
have name
, age
property. Is there a way to avoid repetitions in this scenario?
Yes, you bait there is, then functions comes to your mind right because Functions are means of storing data in this case related data to be called several times generating a new output.
Solution 1: Function
// First way of using a function
function user(name, dob) {
return {
name,
dob,
age() {
let today = new Date().getFullYear();
return today - dob;
},
};
}
let user1 = user("John", 1999);
let user2 = user("Sally", 2002);
user1.age(); // 21
user2.age(); // 18
If you're not used to the syntax above, I've re-declared it to another syntax.
Syntax above: Object Property Value Shorthand in JavaScript with ES6
But If you understand what I did click here to skip the other syntax.
// Second way of using a function
function user(name, dob) {
return {
name: name,
dob: dob,
age: function () {
let today = new Date().getFullYear();
return today - dob;
},
};
}
let user1 = user("John", 1999);
let user2 = user("Sally", 2002);
user1.age(); // 21
user2.age(); // 18
Here another way of writing the user
function above:
// Third way of using a function
function user(name, dob) {
let newUser = {};
newUser.name = name;
newUser.dob = dob;
newUser.age = function () {
let today = new Date().getFullYear();
return today - newUser.dob;
};
return newUser;
}
let user1 = user("John", 1999);
let user2 = user("Sally", 2002);
user1.age(); // 21
user2.age(); // 18
Here's a rundown of the third way of using the function above:
- A function
user
is declared and stored in the Global Memory. user1
has an invocation touser()
function and holds what's returned by the function.name
anddob
parameter inuser
function holds the argument values"John"
and1999
respectively.newUser
has the value of an empty objectnewUser.name = name
:newUser
which is an object has a property namedname
which holds thename
parameter value ("John").newUser.dob = dob
same procedure asnewUser.name = name
newUser.age
holds a function/method. Wherebyage
is the property name.
return newUser
:newUser
object is returned touser1
Same procedure for
user2
.
user1.age()
: Sinceuser1
holds all properties and method fromnewObject
it has access toage()
method which returns21
when the instructions in the method is executed.
So now, we've been able to store personal specific details/data using a Function to which returns an object for a particular user with properties and method.
But there is a problem with this approach, what if we'll like to add another functionality (method) to the returned object in the function. Then we'll have to go to the function to add that functionality (method) which isn't efficient.
Also, most importantly the method age()
is being copied twice, to each user - user1
and user2
which is a waste of memory space since it isn't changing, let alone if the object had hundreds of methods to be called.
With that being said, this approach isn't good enough. So the big question now is, is there a better way?
Solution 2: Prototype Chain
Well yes, there is a better way, which brings about Prototype Chain.
Prototype Chain is a way to link/bond to another object where a method/properties could have been declared, which is then accessed through another object.
Hmm! You might be confused, don't worry I'll break the point down.
Meanwhile, to use the prototype chain, we have to call Object.create()
technique.
function user(name, dob) {
let newUser = Object.create(ageCalc);
newUser.name = name;
newUser.dob = dob;
return newUser;
}
const ageCalc = {
age: function () {
let today = new Date().getFullYear();
return today - this.dob;
},
};
let user1 = user("John", 1999);
let user2 = user("Sally", 2002);
user1.age(); // 21
user2.age(); // 18
Using Object.create(ageCalc)
returns a new object but also has a bond/link to ageCalc
object through a hidden property in all objects known as __prop__
(proto property).
Note: Firefox browser calls
__proto__
prototype<prototype>
Here's what happens when the instructions above are executed
So when the function
user()
is invoked fromuser1
variable, a new Execution Context is created and the functionuser()
is added to the Call Stack.newUser
variable holds the value of an empty object{}
but has a bond withageCalc
object, having access to the method stored there.newUser.name
means create a key / property name called name.
ThennewUser.name = name
means assignname
(as value) got from theuser()
function parameter.
Same procedure fornewUser.dob = dob
.return newUser
newUser is returned touser1
variable making it now initialized withnewUser
object.Since the function
user
is done executing, it popped off the Call Stack.
Same process is also carried out for
user2
variable.
user1.age()
, JavaScript engine checksuser1
object which was addressed asnewUser
in the functionuser
, do I have a method known asage()
in here, but no! so it then goes to the__proto__
(proto property) if there exist any method like that.
The process of checking the proto property is known as the Prototype Chain.
Yes, there is method age()
here, which makes user1.age()
work by invoking the method and returning 21
.
Same process goes for
user2.age()
If you'd notice in the ageCalc
object property age
, there is a keyword this
. The this
keyword makes both user1
and user2
have access to dob
property.
Which is written like this in JavaScript interpreter:
today - user1.dob;
today - user2.dob;
To learn more about the
this
keyword read this article here. by Keshav jha
Note: newUser
object returned already has access to ageCalc
object so user2
doesn't have to create another method/functionality in other to use age()
.
This method of encapsulating properties and method is great, we achieve the goal of not replicating our methods multiple times but instead declare it in memory and have it accessed anytime.
But still, is there a much easier way of having the same goal?
Solution 3: New keyword
The new
keyword does most of the hard work for us and makes writing much easier.
Here are the hard works we don't have to do ourselves anymore using the new
keyword:
- Create a new object (in this case a new user object)
- Return the new user object created
So when the function is being called, it will be written like this:
let user1 = new user("John", 1999);
let user2 = new user("Sally", 2002);
You might wonder, since we aren't creating the object ourselves, how would we have access/link to age
method.
Well it turns out, all functions have an object by default and all functions object has a property called prototype
in them which is also an object.
You can try this for yourself on the console.
function functionObj() {
return "I'm a function";
}
functionObj.stored = "I'm also an object";
functionObj(); // "I'm a function"
functionObj.stored; // "I'm also an object"
functionObj.prototype = {};
Well, the main point here is prototype
property can be used to store methods and properties.
function user(name, dob) {
this.name = name;
this.dob = dob;
}
user.prototype.age = function () {
let today = new Date().getFullYear();
return today - this.dob;
};
let user1 = new user("John", 1999);
let user2 = new user("Sally", 2002);
user1.age(); // 21
user2.age(); // 18
Here's a break down of the code above:
function
user()
is declareduser.prototype.age
: in the functionuser
object which has the propertyprototype
add the propertyage
which takes a function/method.user1 = new user("John", 1999)
: create an Execution Context for the functionuser
. Then the arguments are assigned to their respective properties.new user("John", 1999)
thenew
keyword has created an object- Notice,
this
keyword in the functionuser
body. The object created by thenew
keyword is whatthis
keyword is referring to. - All properties in
user
function are added to an object and returned touser1
.
user1.age()
: the returned object byuser
function has access/link to theage
method through the prototype so thoseuser1
since technically its has the object fromuser
.
Hmm! we've come a long way, but there's still one more way of writing the above instructions. Is there anything wrong with the approach of using the new
keyword?
No, not really. As a matter of fact, the next solution uses the new
keyword but instead of having a function user
and a method on the function's prototype could it all be written as one entity.
Solution 4: Class
Yes, it can. This brought about the class
keyword, which is a syntactic sugar to solution 3.
Example using a Class syntax:
class Users {
constructor(name, dob) {
this.name = name;
this.dob = dob;
}
age() {
let today = new Date().getFullYear();
return today - this.dob;
}
}
let user1 = new Users("John", 1999);
let user2 = new Users("Sally", 2002);
user1.age(); // 21
user2.age(); // 18
From the instructions above,
class User {}
wrapsconstructor()
method and a methodage()
constructor(name, dob) {...}
is basically the same asuser
function at solution 3age()
method declared inclass User {}
is still going to added to the prototype like at solution 3.
Using the class syntax is the standard, but it's important to know other means in order to debug code efficiently.
So the proper way of achieving our goals since the beginning of this article is by using the class
syntax - solution 4.
Conclusion
All the techniques we've covered are aimed at achieving a better way of storing data (properties) and functionalities (method) together in one place which can be used multiple times as need. Also, is the technique easy to reason about, could new functionalities (methods) be added easily and finally is the technique efficient and performant?
That's for you to ponder about...
Here is a video I highly recommend, it covers these concepts further and it's by Ifeoma Imoh
Other articles to read on further include:
- Understanding JavaScript Prototype by Zell Liew
- A conversation with the 'this' keyword in Javascript by Efereyan Karen Simisola
- Execution Context and Call Stack
- Callback Functions and Higher Order Functions
I know it's been a lot, and I hope you've learnt one or two things, Thanks for Reading.