Object Oriented JavaScript
"Everything in JS is an Objects"
We can make a simple object and place methods upon it in order to group together information about a logical object, in our example we will take the user account.
source
var userOne = {
email: "bob@sponge.com",
name: "Bob",
login() {
return this.email + " has logged in";
},
logout() {
return this.email + " has logged out";
}
};
console.log(userOne.login());
Updating Properties
There are two ways of updating properties, we can either address them using the dot notation `object.property`, or, we can address them using square brackets `object["property"]`.
userOne.email = "new@value.set"
userOne["email"] = "another@value.set"
The square bracketed notation can be useful for addressing propertys with a variable. For example if our variable prop is set to "name" it will retrieve the name value, whereas if it is set to "email" it will return the email value. As such the same code can be used to access different propertys of our object.
var prop = "name";
console.log(userOne[prop]);
prop = "email";
console.log(userOne[prop]);
We can also create new properties on objects, by setting their values.
userOne.age = 30;
Creates the age property on userOne.
Classes
We could make a series of objects like this manually but it very quickly becomes laborious, so instead of making them in this way, defining all of the properties and methods every time. To make this simpler there a couple of techniques that we could use, the old method would have been to use prototypes but since ES6 some syntactic sugar has been added to JS in the form of Classes, making this process much cleaner to achieve.
However it is worth bearing in mind that not everyone likes this new class syntax, and as JS does not have true encapsulated classes, they are not always used in code bases as they are in effect just an abstraction over prototypes. With that said we will look at the class syntax now, and return to the prototype syntax a little later on to better under stand it.
To recap, if we only need one object then instantiating it as an object literal is fine, however if we ever need to create multiple versions of an object then Classes or prototypes are a much better way to achieve this.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
var userOne = new User("Bob", "bob@getting.there");
var userTwo = new User("Tim", "tim@been.there");
console.log(userOne);
console.log(userTwo);
Thinking back to our object, we also had login and logout methods, so now we need to see how to define methods upon our class, so that when we create a new user they have those methods available to them.
Notice in the class definition below, that we do not need to finish lines with commas as we do when declaring objects.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
login() {
return this.email + " has logged in";
}
logout() {
return this.email + " has logged out";
}
}
var userOne = new User("Bob", "bob@getting.there");
var userTwo = new User("Tim", "tim@been.there");
console.log(userOne);
console.log(userTwo);
Method Chaining
If we want to run several methods from a class at once this can be done by method chaining. For our User class, we could try to do this by calling one method after, however this will not currently work, we will receive "undefined" if we try to do so.
userOne.login().logout();
To better demonstrate this we will add a new method updateScore and a score property to the user class. Notice how we return this, and instance of itself from the function call, this is what will enable us to chain method calls together.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.score = 0;
}
login() {
console.log(this.email + " has logged in");
return this
}
logout() {
console.log(this.email + " has logged out");
return this
}
updateScore() {
this.score++
console.log(this.email + " score is now " + this.score);
return this
}
}
var userOne = new User("Bob", "bob@getting.there");
var userTwo = new User("Tim", "tim@been.there");
userOne.login().updateScore().updateScore().logout();
Class Inheritance
Supposing that we would like certain users to have a deletUser method, we would not want to add this method to all of our users, only to admin; What we need here is an admin class that implements this method, but we also want all of the other properties and methods from the User class. Rather than copy pasting the content of the User class into the Admin class, we can use inheritance instead.
We create our Admin class as we have done for the User class, and this time we will inherit from the User class upon instantiation, for this we use the extends key word. When extending a class we do not need to use a constructor, the "new" keyword will call the constructor on the extended class instead.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.score = 0;
}
login() {
console.log(this.email + " has logged in");
res.innerHTML += `<pre>${this.email + " has logged in"}</pre>`
return this
}
logout() {
console.log(this.email + " has logged out");
res.innerHTML += `<pre>${this.email + " has logged out"}</pre>`
return this
}
updateScore() {
this.score++
console.log(this.email + " score is now " + this.score);
res.innerHTML += `<pre>${this.email + " score is now " + this.score}</pre>`;
return this
}
}
class Admin extends User {
deleteUser(user) {
users = users.filter(u => {
return u.email != user.email
});
}
}
var userOne = new User("Bob", "bob@getting.there");
var userTwo = new User("Tim", "tim@been.there");
var admin = new Admin("Jane", "jane@school.dito");
var users = [userOne, userTwo, admin];
console.log(users);
admin.deleteUser(userTwo);
console.log(users);
Constructors
As previously mentioned, JavaScript as a language does not actually have classes, the code that we have been writing is simply syntactic sugar on top of already existent functionality of the prototype model.
function User(name, email) {
this.name = name;
this.email = email;
this.online = false;
this.login = function() {
console.log(this.email + " has logged in");
res.innerHTML += `<pre>${this.email + " has logged in"}</pre>`
return this
}
}
var userOne = User("Bob", "bob@getting.there");
var userTwo = User("Tim", "tim@been.there");
console.log(userOne);
We are getting the same object as when we call the class using the new keyword, but this time rather that the constructor being called we are doing the same with a function.
When we want to add methods to an emulated class like this, we don"t usually put those methods inside the constructor function.
Prototype
In most cases it is better to use the prototype to add methods to our sudo class, when we observe an object in the browser console, opening it up we see after its property members another value, prototype.
When we make objects using the class and new keywords, the methods that we attach are found within the object inside of this prototype section, however when we obsere the users made in the code above, for which we initaited the method with the object itself, the method is not found in the prototype but is directly underneeth the Object allong with its properties.
If we want to generate a sudo class in the same way that the Class key word does, we will need to add the methods to the prototype not to the first level of properties.
So what is a prototype anyway?
Every object type has a prototype, an array has a specific prototype, a date object has a specific prototype, any custom object or type that we try to emulate will have a prototype. This prototype of an object in JavaScript is like a map for that object type. The prototype will contain all of the methods or preset values that are required by that type such that all instances of that type will point to its prototype insuring that they remain the same.
So how do we access the prototype property? Simply by way of the dot notion and the name prototype.
userOne.prototype = function(){};
If we make an object and then add methods to it using this technique, we can see that the methods are found within the prototype property.
var res = document.querySelector("res09");
function User(name, email) {
this.name = name;
this.email = email;
this.online = false;
}
User.prototype.login = function() {
this.online = true;
console.log(this.email + " has logged in");
}
User.prototype.logout = function() {
this.online = false;
console.log(this.email + " has logged out");
}
var userOne = new User("Bob", "bob@getting.there");
console.log(userOne);
When we use the class keyword and new to instantiate object instances, JavaScript is doing all this behind the scenes so that we don"t have to do it manually.
Prototype Inheritance
We have seen how useful for creating more advanced classes inheritance can be, when we made our class Admin, next we will see how to achieve the same using Objects.
apply and call
Once we are inside of a function, instatiating an object we can add the structure of another object by calling it with either the apply or the call key words. Into which we pass the this keyword, which withing the functions scope is refering to the object that is currently being instantiated.
To pass through arguments to our new type, we use the ... notation
effectivly making the arguments and array, the array is then passed
into the function all along with this<\code>, as the second
argument to our Objects apply method.
function Admin(...args) {
User.apply(this, [name, email]);
}
function User(name, email) {
this.name = name;
this.email = email;
this.online = false;
}
User.prototype.login = function() {
this.online = true;
console.log(this.email + " has logged in");
}
User.prototype.logout = function() {
this.online = false;
console.log(this.email + " has logged out");
}
function Admin(...args) {
User.apply(this, args);
this.role = "admin";
}
var userOne = new User("Bob", "bob@getting.there");
var userTwo = new User("Tim", "tim@been.there");
var admin = new Admin("Jane", "jane@school.dito");
console.log(admin);
Currently if we look inside of the our admin object we do not yet have the login and logout function of the user, we need to inherite these as well.
Remembering that the metods from the User type are attached to its
prototype, to do this we use the built in function
Object.create(User.prototype)
to create the object upon
our target prototype.
admin.prototype = Object.create(User.prototype);
Adding this to our code, we find the missing functions in our Admin objects prototype.
function User(name, email) {
this.name = name;
this.email = email;
this.online = false;
}
User.prototype.login = function() {
this.online = true;
console.log(this.email + " has logged in");
res.innerHTML += `<pre>${this.email + " has logged in"}</pre>`
}
User.prototype.logout = function() {
this.online = false;
console.log(this.email + " has logged out");
}
function Admin(...args) {
User.apply(this, args);
this.role = "admin";
}
var userOne = new User("Bob", "bob@getting.there");
var userTwo = new User("Tim", "tim@been.there");
Admin.prototype = Object.create(User.prototype);
var admin = new Admin("Jane", "jane@school.dito");
console.log(admin);
The Admin object now has an additional prototype inside, that of the User object, we might wonder why it does not fill the Admin"s current prototype, it is because we need the Admins own prototype, lest we decide to extend the class.
var userOne = new User("Bob", "bob@getting.there");
var userTwo = new User("Tim", "tim@been.there");
Admin.prototype = Object.create(User.prototype);
var admin = new Admin("Jane", "jane@school.dito");
var users = [userOne, userTwo, admin];
admin.prototype.deleteUser = function(u) {
users = users.filter(user => {
return user.email != u.email;
});
}
console.log(users);
admin.deleteUser(users[0]);
console.log(users);