In this post I will try to cover how OOP applies in JavaScript. We are going to explore the prototype chain, see what the new
keyword really does, and what defining a class
means in JS. Let's dive in.
There are loads of books/articles/blogs about Object Oriented Programming, so I am not going to reiterate what's already out there or debate its pros and cons. If you are interested in the paradigm, check out this article on Wikipedia.
In the simplest terms, an object (in OOP) is a data structure that contains data and methods that can modify that data.
If you are a JavaScript programmer, then you will feel right at home with the concept of Objects. Creating Objects is no big deal in JS.
const book1 = {
name: "Curtains",
author: "Agatha Christie",
genre: "Mystery",
checkouts: 0,
incrementCheckouts: function(){
book1.checkouts++;
}
}
This code block shows a way to create an object. The fields: name
, author
, genre
and checkouts
represent the data and incrementCheckouts
is the encapsulated method that modifies the data.
Now, you may be wondering if this is it. Well, yeah. But there are issues with creating objects like this.
Suppose we have a library and each book needs to be tracked, categorized and tagged. If we create simple objects like this, we will be rewriting each object manually. Plus, it wont be memory efficient either.
What can we do to make this DRY?
Time to write a function that generates these book objects.
function createBook(name, author, genre, checkouts=0) {
const book = {};
book.name = name;
book.author = author;
book.genre = genre;
book.checkouts = checkouts;
book.incrementCheckouts = function(){
book.checkouts++;
}
return book;
}
Now we can call the function createBook
to generate the book object.
const book1 = createBook("Curtains", "Agatha Christie", "Mystery");
Awesome! Let's create some more books.
const book2 = createBook("The Shinning", "Stephen King", "Thriller");
const book3 = createBook("Inferno", "Dan Brown", "Suspense");
...
As we add more books and methods to our book Object, an interesting problem bubbles up. If you notice, each time you create a book using the generator, a new instance of the incrementCheckouts
method is also created in the memory. If we had several methods like for marking a book as unavailable or checked out, the memory footprint would be unnecessarily large since each book will have copies of the same methods. To solve this, each method should be passed by reference.
This is not a hard problem to solve. Here is one way of optimizing the generator:
const bookMethods = {
incrementCheckouts: function(){this.checkouts++},
//add several other methods
}
function createBook(name, author, genre, checkouts=0) {
const book = Object.create(bookMethods);
book.name = name;
book.author = author;
book.genre = genre;
book.checkouts = checkouts;
return book;
}
What is Object.create()
? First, allow me to introduce you to a hidden property in JavaScript Objects: the __proto__
(FYI __
is pronounced dunderscore, i.e double underscore).
When you call Object.create()
, whatever object you pass in the argument, it is linked to the __proto__
property of the resulting object.
This allows you to use the methods/properties that are passed as the argument object via the dot notation.
If we create a book using this new generator, we would be able to access the incrementCheckouts
method, like we used to.
const book1 = createBook("Curtains", "Agatha Christie", "Mystery");
book1.incrementCheckouts();
console.log(book1);
The result is as we expected.
But how does that happen?
This is called the prototype chain. The interpreter tries to find the incrementCheckouts
property on the book1 object. When it is not there, it moves one level up the chain via the __proto__
and finds it in the bookMethods object.
new
KeywordThe new
keyword was introduced to make this easier. But as abstraction increases, the underlying complexity does as well.
With new
, the code can be rewritten as
function CreateBook(name, author, genre, checkouts=0) {
this.name = name;
this.author = author;
this.genre = genre;
this.checkouts = checkouts;
}
CreateBook.prototype.incrementCheckouts = function(){
this.checkouts++
};
const book1 = new CreateBook("Curtains", "Agatha Christie", "Mystery");
Here is what new
does:
this
.__proto__
property of the empty object to the prototype object in the parent object.this
object.The benefit of using this approach is that it is slightly faster to write and it is fairly standard practice in real world code.
A con of using this is if you forget to call the new
keyword, the resulting object would be undefined
. To avoid this, most programmers capitalize the first letter of the creator function to relay others that this function has to be called with the new
keyword.
Also, another problem arises if you decide to create a new function within one of the prototype methods.
CreateBook.prototype = function(){
print(){
console.log(this.checkouts++)
}
this.checkouts++;
print();
}
Something you should keep in mind, this
refers to the context of the function that calls the method i.e the object preceding the dot. Since the print function is called without this notation, this
is instead pointing to the global context (window
in the browser).
According the Will Sentance from CodeSmith, this is considered as the biggest 'gotcha' in JavaScript's Object Oriented Programming.
To fix this, you can use the arrow functions which conserve the value of this
where it is decided by the enclosing lexical scope. Before arrow functions, we had to call the .bind method to set the context of the method.
To learn more about the this
keyword, go to this MDN link.
class
keywordTo further abstract OOP in JS, we were provided with the class
keyword. Again, what happens underneath is unknown to most developers. But since we have built our way sequentially, this should be cake for you to interpret.
Here is how we can rewrite what we did earlier
class Book{
constructor(name, author, genre, checkouts=0){
this.name = name;
this.author = author;
this.genre = genre;
this.checkouts = checkouts;
}
incrementCheckouts = function(){
this.checkouts++
}
}
const book1 = new Book("Curtains", "Agatha Christie", "Mystery");
The constructor is simply a replacement for the original function:
function CreateBook(name, author, genre, checkouts=0){
this.name = name;
this.author = author;
this.genre = genre;
this.checkouts = checkouts;
}
The prototype functions are instead listed as methods inside the class.
And we have already gone over what new
does.
This means you now understand the inner workings of Object Oriented Programming in JavaScript.
Give yourself a pat on the back.
I hope this was useful to you. My purpose was to reinforce my own learning and also, to help anyone else who wants to wrap their heads around the underlying workings of JavaScript.
Thanks for reading and have a good day.