Nov 2, 2023

Deep Dive into Object Oriented Programming (OOP) in JavaScript

  • #javascript

Explore how Object Oriented Programming (OOP) works in JavaScript, covering history, objects, prototypes, and ES6 classes

Deep Dive into Object Oriented Programming (OOP) in JavaScript

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. These choices ultimately shaped JavaScript’s unique identity resulting in a prototype-based object model.

Object in JavaScript

Before we try 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. Importantly, JavaScript objects are not just simple key-value stores like hash maps. They can also carry additional metadata and functionality.

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 person
  • lastName - last name of the person
  • age - age of the person
  • greet() - method to greet a person
  • getFullName() - method to get full name of the person
  • isAdult() - 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. Although 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? How can we mimic the concept of an object constructor? Simple! we can create a function, that will create the object 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, the common methods getFullName(), isAdult() and greet() are created on the person object every time we create a new person. So, each person gets an identical copy of the same function. 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

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.

The new 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.