Deep Dive into Object Oriented Programming (OOP) in JavaScript
Explore how Object Oriented Programming (OOP) works in JavaScript, covering history, objects, prototypes, and ES6 classes
Table of contents
You are not the only one to feel that OOP in JavaScript is weird. This is fairly common for people coming from other backgrounds. Many developers transitioning from traditional class-based languages share the same sentiment. JavaScript’s approach to OOP is unique and deviates from the norms of other languages.
In this comprehensive guide, we'll explore the how OOP works in JavaScript, breaking down its core concepts and providing practical examples. It's important to note that this article is not about OOP principles in general.
History
Before we deep dive into technical details, it is important to know the historical context around JavaScript’s creation.
In 1994 Netscape released their web browser called Netscape navigator. The web browser was a major success. Brendan Eich convinced his boss at Netscape that the Navigator browser should have its own scripting language. At that time Java was very very popular. Netscape was negotiating with Sun to include it in Navigator. The big debate inside Netscape therefore became “why two languages? why not just Java?”.
They eventually decided to come up with a new language for their browser. But the diktat from upper engineering management was that the language must look like Java because they wanted to attract Java programmers. At the same time, they didn’t want to compete with Java. So Brendan Eich did something different for JavaScript. He chose first-class functions from Scheme and prototypes from Self as the main ingredients.
What is an Object in JavaScript?
To understand OOP in JavaScript, it's crucial to grasp the concept of objects in JavaScript. The term "object" in JavaScript does not equate to the same concept in other languages. In JavaScript, objects are fundamental, and almost everything is an object.
An object in JavaScript is simply a key-value pair where key is a string and the value can be anything.
It's similar to a data structure called a hash table or hash map, though the underlying implementation can vary across different JavaScript engines. The term "object" can be confusing because we often associate it with classes in other languages. In traditional OOP languages, you can't create objects without classes. However, in JavaScript, objects can be created on the fly without the need for classes.
Creating objects in JavaScript
Just like we can create variables, we can create objects on the fly. An object can be created in several ways. In the examples, we’ll try to model an Object for a person. We’ll have the following properties and methods in the object.
firstName
- first name of the personlastName
- last name of the personage
- age of the persongreet()
- method to greet a persongetFullName()
- method to get full name of the personisAdult()
- method to to check if the person is a legal adult
Object Literals
Let’s start with object literals. With the object literals syntax, you can create objects just with a set of curly braces — {}.
const person = {
firstName: "Naimul",
lastName: "Haque",
age: 26,
getFullName: function () {
return `${this.firstName} ${this.lastName}`;
},
isAdult: function() {
return this.age >= 18;
},
greet: function() {
console.log(`Hello, my name is ${this.getFullName()}`);
}
};
Using Object Constructor
In JavaScript, the new Object()
statement is used to create a new instance of the Object constructor. This essentially creates an empty object. It's equivalent to creating an empty object using object literal notation {}.
const person = new Object();
person.firstName = "Naimul";
person.lastName = "Haque";
person.age = 26;
person.getFullName = function () {
return `${this.firstName} ${this.lastName}`;
};
person.isAdult = function() {
return this.age >= 18;
}
person.greet = function() {
console.log(`Hello, my name is ${this.getFullName()}`);
}
Using Object.create()
The previous 2 methods of creating objects were somewhat regular. Object.create()
creates a new object and returns the empty object, additionally it does one very important thing. We'll come back to it shortly.
// ignore the null value we're passing into Object.create()
// we'll explain this shortly but anyway this creates an empty object
const person = Object.create(null);
person.firstName = "Naimul";
person.lastName = "Haque";
person.age = 26;
person.getFullName = function () {
return `${this.firstName} ${this.lastName}`;
};
person.isAdult = function() {
return this.age >= 18;
}
person.greet = function() {
console.log(`Hello, my name is ${this.getFullName()}`);
}
Issues with our code
Let’s first take a look at the issues that we have with the above 3 approaches we’ve described so far. Think about it, if we want to define more persons, we have to repeat the same codes which is not a good practice.
So what can we do here? Simple! we can create a reusable function, that will create and return these person
objects for us.
function createPerson(firstName, lastName, age) {
const person = {};
person.firstName = firstName;
person.lastName = lastName;
person.age = age;
person.getFullName = function () {
return `${this.firstName} ${this.lastName}`;
};
person.isAdult = function() {
return this.age >= 18;
}
person.greet = function() {
console.log(`Hello, my name is ${this.getFullName()}`);
}
return user;
}
const person1 = createPerson("Naimul", "Haque", 26);
const person2 = createPerson("John", "Doe", 32);
We can create many users with this function. createPerson()
function is now reusable but it's extremely inefficient. The reason is simple, the common methods getFullName()
, isAdult()
and greet()
are created on the person object every time we create a new person. These methods are common functionality and don’t need to have same own copy for each object. We need some way to somehow share the common functionality.
Understanding prototypes and __proto__
prototypes
are JavaScript's way to achieve inheritance. In other words, it’s a way to share common properties and methods to object(s). prototypes are a way by which a JavaScript object can inherit features from another object.
Everything in JavaScript gets a special property called __proto__
. Have you ever thought how we’ve access to the array methods like filter
, map
, forEach
etc? Let’s take a look into that.
const nums = [1, 2, 3, 4, 5];
console.log(nums.__proto__);
nums.__proto__
is a special object that contains all the array methods. Let’s run this code to verify the statement.
You might be thinking, we are supposed to talk about Objects. Why are we talking about arrays? JavaScript wraps every primitive values (numbers, strings etc) in an Object. So, everything in JavaScript is an Object (excluding undefined
and null
) and will have it’s own __proto__
depending on the type of Object. For examples, arrays will have array methods in their __proto__
, numbers will have methods like toFixed
, toString
etc in their __proto__
.
The next question is where does this __proto__
comes from? JavaScript provides us Constructors like Array
, Number
, String
, Object
etc. We can use new Array() or new Number() or new String() or new Object() to create objects of these type. Usually we don’t use these syntax in our day to day life. Whatever way we use, it is eventually treated as an Object created from these Constructors and these Constructors have a special property prototype. Note that, prototype
and __proto__
are not the same in terms of the purpose they server.
The difference is that, prototype exists only in Constructor and __proto__
is meant for the instances created from a constructor. However, there is a close relation between these two objects.
const nums = [1, 2, 3, 4, 5];
console.log(nums.__proto__ === Array.prototype); // true
If you run the above code, you will log true
in the console. Basically nums._proto__
is a reference of Array.prototype
. The most important thing to understand here is that, whenever you create an instance from any Constructor, __proto__
on the instance object will refer to the prototype
of the Constructor.
Now, Let’s assume that we want to access user.isMarried
in the person
object that we defined previously. The property doesn't exist on the person
object. So instead of giving up, JavaScript will try to find it in __proto__
. So anything if JavaScript is unable to find some property or method in the actual object, it will look into the special property called __proto__
.
So that basically means if we can set __proto__
of each person
object to be the commonMethods
object, our job is done. How do we do that? Remember Object.create()
? It returns a new object, but the additional thing it does is, it sets the __proto__
of that object to be the object that we pass into this as argument.
Sharing Common Functionalities
Let’s move all of the methods to another object called commonMethods
.
function createPerson(firstName, lastName, age) {
const person = {};
person.firstName = firstName;
person.lastName = lastName;
person.age = age;
return person;
}
// this is the object consisting of the methods that we want
// to share with all the persons created by the createPerson()
// function without creating a copy for each individual object
const commonMethods = {
getFullName() {
return `${this.firstName} ${this.lastName}`;
},
isAdult() {
return this.age >= 18;
},
greet() {
console.log(`Hello, my name is ${this.getFullName()}`);
}
};
It’s obvious that the person
object doesn’t have any connection with the commonMethods
object. So, we need to somehow create a connection between them in such a way that whenever the person object needs a method, it will look into the commonMethods
objects. How do we do that?
According to our knowledge of prototypes and __proto__
, we can now answer this question. If we can set __proto__
of each person
object to be the commonMethods
object, everything should work fine. Remember Object.create()
? It returns a new object, but the additional thing it does is, it sets the __proto__
of that object to be the object that we pass into this as argument.
function createPerson(firstName, lastName, age) {
// it creates a new object called person and sets
// the person object's __proto__ to be the commonMethods
const person = Object.create(commonMethods);
person.firstName = firstName;
person.lastName = lastName;
person.age = age;
return person;
}
const commonMethods = {
getFullName() {
return `${this.firstName} ${this.lastName}`;
},
isAdult() {
return this.age >= 18;
},
greet() {
console.log(`Hello, my name is ${this.getFullName()}`);
}
};
const person = createPerson('Naimul', 'Haque', 26);
person.greet(); // Hello, my name is Naimul Haque
It works perfectly! You might be thinking, this is somewhat similar to how a class
creates an object in other programming languages. Don’t we have the class
keyword in JavaScript? Yes, we have the class
keyword, and we'll get there eventually. The reason I'm showing you all these in details is because, class
is just a syntactic sugar in JavaScript. It didn’t even exist before ES6. Even if you write classes, it's crucial to understand the underlying concepts.
Functions are Objects
As I discussed in earlier section, that we can think everything as Objects in JavaScript. Functions are also not different. They are Objects. We can attach properties to functions just like a regular Object. The following code looks a bit strange but it is indeed valid in JavaScript.
function sum(a, b) {
return a + b;
}
sum.myName = "Naimul Haque";
console.log(sum.myName);
Constructor Functions
Do you remember about new Number()
, new String()
, new Array()
, new Object()
? As I mentioned previously, JavaScript provides us Constructor functions, and we can use the new
keyword to create instances from these constructor functions. In the previous section, we’ve created a function called createPerson, however this is just a regular factory function, not a constructor function.
In JavaScript, a constructor function is a special type of function that is used to create and initialize objects. The constructor function is used with the new
keyword to create instances of objects similar to how we use classes in other programming languages. Now, we’ll try to convert that function into a constructor function. Let’s take a look at the following code.
function Person(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
Person.prototype = {
getFullName() {
return `${this.firstName} ${this.lastName}`;
},
isAdult() {
return this.age >= 18;
},
greet() {
console.log(`Hello, my name is ${this.getFullName()}`);
}
};
// create an instance of the Person
const person1 = new Person("Naimul", "Haque", 26);
The function Person
is our constructor function, and when we use the new
keyword in front of it, it will create an instance every time we do that.
We have already seen that functions are objects. Constructors can have a special property called prototype
. It's an object and when we attach any property to this prototype
object, the new
keyword will use this to set the object's __proto__
to be the constructor function's prototype
. Can you remember? I mentioed the same thing in the prototypes section. The only difference is that, this constructor is created by us, whereas Number
, String
, Array
, Object
etc are provided by JavaScript.
Now, each time we create a Person instance, the object’s __proto__
will refer to the Person.prototype
. We can validate the statement with the following code.
person1.__proto__ === Person.prototype // true
Again, if you are finding it difficult, read the prototype section once again, come back and try to understand.
Thenew
Keyword
function Person(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
The Person
function used with the new
keyword can be confusing for a lot of people. You might already have questions in your mind.
What is the value of
this
?Where does the
this
comes from?How does the person getting returned from the function even though there is no
return
statement?
We will get all the answers, when we take a look at how the new
keyword works in JavaScript.
Creates a new empty object.
Sets the object’s prototype (
__proto__
) to be the function's prototype.Invokes the constructor function where
this
refers to the empty object that it created.Implicitly returns the object.
This is how, the instances are getting created from the Person
constructor functions. However, the value of the this
keyword is dynamic and can change upon different contexts. If you want to learn more about the this
keyword, I’ve written an Article explaining it in depth.
A closer Look at the ‘this’ keyword in JavaScript
ES6 Classes
There were not class
keyword in JavaScript before ES6. The ES6 brings the class
keyword that everyone is familiar with. It's just a syntactic sugar over the already existing way for creating objects with constructor functions
or Object.create()
.
class Person {
constructor(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
isAdult() {
return this.age >= 18;
}
greet() {
console.log(`Hello, my name is ${this.getFullName()}`);
}
}
// Create an instance of the Person class
const person1 = new Person("Naimul", "Haque", 26);
It looks more like a classical OOP language, easier to read, write and understand. As I mentioned, this is just a Syntactic sugar. Behind the scences, JavaScript does all sorts of work to create Person.prototype
, __proto__
, put those methods in the __proto__
in every instance of the person. In fact, the class
we created named Person
is a actually a function
.
Conclusion
I hope you got a better understanding of how Object Oriented Programming works in JavaScript. Thank you for reading.