Object Oriented Programming in JavaScript

JavaScript is an Object Oriented Programming language. At least—it can be! Check out this article about how to use Object Oriented Programming concepts to structure your JavaScript code.

Much of what you do when writing software is creating and connecting multiple values and methods that work together to provide your application's functionality. Object-oriented programming helps you achieve and implement this much more easily and declaratively.

In this article, you will learn everything you need to know to start using classes and object-oriented programming approaches in JavaScript.

Let's examine what you need to get the most out of this article.

Prerequisites

To follow this tutorial, you need to have basic knowledge of JavaScript.

Object-Oriented Programming

Object-oriented programming (OOP) is a programming paradigm used by developers to structure software applications into reusable pieces of code and blueprints using objects. OOP is used daily in large, complex, and actively maintained projects.

OOP makes it easier to create and manage many parts of your application in isolation and connect them without making them depend on one another. Next, let's look at the four major concepts of OOP.

Abstraction

Abstraction in OOP is the process of exposing only the necessary functions to the user while hiding the complex inner workings to make the programs easier to use and understand.

For example, when you send a message on your phone, all the functions and logic that routes your message to the other person are hidden because you don't need to know them or how they work.

Similarly, in programming, if you are building an API that helps financial apps verify users identity and banking information, the developers that use your API don't need to know which database system you use or how you make calls to your database. All they need to know is the function they need to call and what parameters they need to provide.

Abstraction helps to reduce complexity, increases usability, and makes changes to the application less disruptive.

Encapsulation

Encapsulation is the process of bundling related code into a single unit. Encapsulation makes it impossible for other parts of the code to manipulate or change how the bundled part of the application works unless you explicitly go into that unit and change them.

For example, if you are building a flight-booking API, it makes sense to separate the code that searches for the flight from the code that books the flight. This way, two different developers can work on each part seamlessly without conflicts because each developer will have no reason to manipulate the other's code directly.

Encapsulation helps you reduce complexity and increases code reusability.

Inheritance

Inheritance in OOP reduces code duplication, enabling you to build a part of your application on another by inheriting properties and methods from that part of the application.

For example, when building an e-commerce delivery app with multiple types of vehicles, the Car and Motorcycle classes can inherit pickUp and dropOff functions from the Vehicle class. Classes will be explained in more detail later in the article.

A significant advantage of inheritance in OOP is the reduction of code duplication.

Polymorphism

In programming, polymorphism is a term used to describe a code or program that can handle many types of data by returning a response or result based on the given data.

For example, you have a form that is used to add products to a product catalog and three different types of products. With polymorphism, you can create a single class method to format all kinds of products before adding them to the database.

Polymorphism helps you eliminate complex and unnecessary if and switch statements, as they can become lengthy when writing complex programs.

Let's look at JavaScript objects in the next section.

JavaScript Objects

An object in JavaScript is an unordered collection of key-value pairs, also known as properties and values. Object keys can be a string value, while the values can be any type. Here are some examples:

  • String
  • Number
  • Boolean
  • Array
  • Function

Next, let's look at how to create objects in JavaScript.

Creating Objects

Creating an object in JavaScript is fairly easy:

const car = {
    name: 'Ford',
    year: 2015,
    color: 'red',
    description: function () {
    return `${this.name} - ${this.year} - ${this.color}`;
    }
} 

The code above declares a car object with name, year, color, and a description function as its properties.

Accessing Object Properties

There are two ways to access an object property in JavaScript; let's look at them below:

Using the Dot Notation

The following example shows how to access object properties using dot notation.

const country = {
    name: 'Spain',
    population: 4000000,
    description: function () {
    return `${this.name} - ${this.population}`;
    }
}

If you have an object like the one shown above, you can use the format objectName.keyName, which should return the value of the given key:

console.log(country.name); // returns 'Spain'

Using the Array Notation

The following example shows how to access object properties using the array notation.

const job = {
  role: "Software Engineer",
  'salary': 200000,
  applicationLink: "meta.com/careers/SWE-role/apply",
  isRemote: true,
};

If you have an object like the one above, you can use the format objectName[keyName], which should return the value of the given key:

console.log(job[role]); // returns 'Software Engineer'

Additionally, you can only access the salary property using the array notation. Trying to get it with the dot notation will return an error:

console.log(job.'salary'); // SyntaxError: Unexpected string

Next, let's see how to modify object properties.

Modifying Object Properties

You can dynamically add, edit, and delete object properties in JavaScript.

Editing Properties

You can use the assignment = operator to modify object values. Here is an example:

const person = {
  name: "John",
  age: 30,
  job: "Software Developer",
  country: "Nigeria",
  car: "Ford",
  description: function () {
    return `${this.name} - ${this.age} - ${this.job.role} - ${this.country.name} - ${this.car.name}`;
  },
};

You can also change the value of name in the above object:

person.name = "Smith Doe";
console.log(person.name); // returns "Smith Doe"

Adding New Properties

One of the significant differences between objects in other languages and objects in JavaScript is the ability to add a new property to an object after creation.

To add a new property to an object, you use the dot notation:

// adding a new `race` property
person.race = "Asian";
console.log(person.race); // returns "Asian"

The code above adds a new race property with the value "Asian".

Deleting Object Properties

JavaScript allows you to delete properties from an object by using the delete keyword:

delete person.race;
console.log(person.race); // returns 'undefined'

The code above deletes the race property, and accessing the race property will return undefined.

Note: You can only delete existing object properties.

Checking Properties

Before adding to or deleting properties from an object, it is an excellent idea to determine whether the property exists on the object. This seemingly simple check will save you hours of debugging a bug caused by duplicate values.

To determine whether a property exists on an object, you can use the in keyword:

console.log('name' in person) // returns true
console.log('race' in person) // returns false

The code above returns true for the name check because the name exists and false for the deleted race property.

Now that you know what objects are and how to use them, let's take the next step to OOP in JavaScript by learning about classes.

Classes

In programming, a class is a structure defined by a programmer that is then used to create multiple objects of the same type. For example, if you are building an application that handles various cars, you can create a Car class that has the essential functions and properties that apply to all vehicles. Then, you can use this class to make other cars and add properties and methods that are specific to each vehicle to them.

To extend the objects you saw in the previous example, if you wanted to create another job object, you would need to create something like this:

const job2 = {
  role: "Head of Design",
  salary: 175000,
  applicationLink: "amazon.com/careers/hod-role",
  isRemote: false,
};

However, as you can see, the above way of creating multiple objects is error-prone and not scalable because you can't make 100 job objects by writing that out every time you need to create a job. This is where classes come in handy.

Creating Classes

You can create a Job class to simplify creating multiple jobs:

class Job {
  constructor(role, salary, applicationLink, isRemote) {
    this.role = role;
    this.salary = salary;
    this.applicationLink = applicationLink;
    this.isRemote = isRemote;
  }
}

The code above creates a Job class with role, salary, applicationLink, and isRemote properties. Now you can create different jobs using the new keyword:

let job1 = new Job(
  "Software Engineer",
  200000,
  "meta.com/careers/SWE-role/apply",
  true
);

let job2 = new Job(
  "Head of Design",
  175000,
  "amazon.com/careers/hod-role",
  false
);

The code above creates two different jobs with all the required fields. Let's see if this works by printing both jobs out in the console:

console.log(job1);
console.log(job2);

The above code should print out both jobs in the console:

job1 and job2 in the console

The image shows both jobs and all their properties in the console.

The this Keyword

The this keyword is considered the most confusing keyword in JavaScript because it means different things depending on where it appears in your code.

In the above example, the this keyword refers to any object created with the Job class. Therefore, inside the constructor method, this.role = role; means "define the role property of this object you're creating as any value given to the constructor".

Also, note that the initial values you give must be in order when creating a new Job object. For example, you create a job3 object like so:

let job3 = new Job(
  "netflix.com/careers/HOE-role",
  true,
  "Head of Engineering"
);

console.log(job3)

The code above creates a new job3 object with the wrong order of the properties and is missing the isRemote property. You would get something like this in the console:

incomplete properties

The image above shows what the job3 object will look like when printed out in the console. Notice that the isRemote is undefined.

In the next section, let's look at how to add methods to classes.

Class Methods

When creating classes, you can add as many properties as you like. For example, if you have a Vehicle class, aside from basic properties like type, color, brand, and year, you probably also want to have methods like start, stop, pickUpPassengers, and dropOffPassengers.

To add methods inside classes, you can add them after the constructor function:

class Vehicle {
  constructor(type, color, brand, year) {
    this.type = type;
    this.color = color;
    this.brand = brand;
    this.year = year;
  }
  start() {
    return "Vroom! Vroom!! Vehicle started";
  }
  stop() {
    return "Vehicle stopped";
  }
  pickUpPassengers() {
    return "Passengers picked up";
  }
  dropOffPassengers() {
    return "Passengers dropped off";
  }
}

The code above defines a Vehicle class with type, color, brand, and year properties, as well as start, stop, pickUpPassengers, and dropOffPassengers methods.

To run methods on the object, you can use the dot notation:

const vehicle1 = new Vehicle("car", "red", "Ford", 2015);
const vehicle2 = new Vehicle("motorbike", "blue", "Honda", 2018);

console.log(vehicle1.start()); // returns 'Vroom! Vroom!! Vehicle started'
console.log(vehicle2.pickUpPassengers()); // returns "Passengers picked up"

The code above creates two types of vehicles using the Vehicle class and runs the class methods on them.

Let's look at computed properties in the next section.

Computed Properties

Programming largely depends on changing values, so similar to how you don't want to hardcode most of the values of the class properties, you might have some dynamic property names that vary based on some values. You can use computed properties to do that; let's see how.

For example, when creating a job listing API, you might want developers to be able to change the applyThrough function name to another word, such as applyThroughLinkedIn or applyThroughIndeed, depending on their platform. To use computed properties, you need to wrap the property name in square brackets:

let applyThrough = "applyThroughIndeed";

class Job {
  constructor(role, salary, applicationLink, isRemote) {
    this.role = role;
    this.salary = salary;
    this.applicationLink = applicationLink;
    this.isRemote = isRemote;
  }
  [applyThrough]() {
    if (applyThrough === "applyThroughLinkedin") {
      return `Apply through Linkedin`;
    } else if (applyThrough === "applyThroughIndeed") {
      return `Apply through Indeed`;
    }
  }
}

The code above declares the applyThrough variable with the string value of "applyThroughIndeed" and a computed method [applyThrough] that can be called job1.applyThroughIndeed().

Computed properties make it easier for other developers to customize their code.

Getters and Setters

When writing code in a team, you want to limit who can change certain parts of the codebase to avoid bugs. It is advisable that in OOP, certain variables and properties should be hidden when necessary.

Next, let's learn how the get and set keywords work.

Getters

When building apps that are keen on ensuring users' privacy, for example, legal issues management apps, you want to control who can access users' data like names, emails, and addresses. The get keyword helps you achieve this. You can limit who can access information:

class Client{
  constructor(name, age) {
    this._name = name;
    this._age = age;
  }
  get name() {
    if (userType === "Lawyer") {
      return this._name;
    } else {
      return "You are not authorized to view this information";
    }
  }
}

The code above declares a Client class with properties name and age and a getter that only returns the name if the user is a Lawyer. If you try to access the name as an Assistant, you'll get an error:

let userType = "Assistant";
const person = new Client("Benjamin Adeleye", 24);
console.log(person.name); // returns 'You are not authorized to view this information'

Note: The this.name is changed to this._name to avoid naming collisions.

Let's learn about setters next.

Setters

The set keyword is the opposite of the get keyword. The get keyword is used to control who can access properties, while the set keyword controls who can change the value of properties.

To learn how the set keyword works, let's extend the previous example by adding a setter:

  set name(newName) {
    if (userType === "Lawyer" && verifiedData === true) {
      this._name = newName;
    } else {
      console.log("You are not authorized to change this information");
    }
  }

The above code declares a set method that allows changes to the name only if the user is a Lawyer and documents have been verified:

let userType = "Lawyer";
let verifiedData = false;
let client = new Client("Benjamin Adeleye", 30);
client.name = "Adeleye Benjamin";
console.log(client.name); // returns 'You are not authorized to change this information'

Note: Methods prefixed with the get and set methods are called getters and setter functions, respectively.

Next, let's take a look at static properties and methods.

Static Values

Sometimes you want to create properties and methods bound to the classes and not the class' instances. For example, you might want a property that counts the number of clients in the database, but you don't want that value bound to the class instances.

Let's look at static properties next.

Static Properties

To track the number of clients in the database, you can use the static keyword:

static clientCount = 0;

The code above declares a static clientCount property with the value of 0. You can access static properties like this:

let cl = new Client("Benjamin Adeleye", 30);
console.log(Client.clientCount); // returns 0

Note: Trying to access static properties using console.log(cl.clientCount); would return undefined because static properties are bound to the class and not instances.

Next, let's look at static methods.

Static Methods

Creating static methods is very similar to creating static properties because you only need to prefix the method name with the static keyword:

  static increaseClientCount() {
    this.clientCount++;
  }

The code above declares a static increaseClientCount method that increases the clientCount every time it is called.

Static methods and properties make it easy to create utility and helper functions that can be used directly on the class and not the instances.

Private values

The ECMAScript2022 update shipped with support for private values inside JavaScript classes.

Private fields and methods take encapsulation in classes to the next level because you can now create properties and methods that can only be used inside the class declaration curly braces, and any code outside those curly braces won't be able to access them.

Let's look at private properties next.

Private Properties

You can declare private properties inside a class by prefixing the variable with the # sign. Let's improve the Client class in the sections by adding a unique id for every client:

class Client {
  #client_unique_id = "";
  constructor(name, age, id) {
    this._name = name;
    this._age = age;
    this.#client_unique_id = id;
  // same as Client class...
  }

The code above declared a private #client_unique_id variable that can only be used and accessed inside the class declaration. Trying to access it outside the class will return an error:

let cl = new Client("Benjamin Adeleye", 30, "##34505833404494");
console.log(cl.name);
console.log(cl.#client_unique_id); // returns Uncaught SyntaxError: Private field '#client_unique_id' must be declared in an enclosing class

Let's look at private methods next.

Private Methods

As mentioned earlier, private methods are only accessible inside the class declaration. To learn how private methods work, we will add a private method that fetches the client case file documents from the database:

   #fetchClientDocs(id) {
    return "Fetching client with id: " + id;
  }

The code above can now be used inside a public function that the user will call to get the client documents. The essence of the private function is to hide all the underlying authentication and calls to the database from the user or developer that uses the code.

Note: You can also create private static, getter, and setter functions.

Next, let's learn how to chain class methods in the next section.

Method Chaining

As a developer, one of the things you probably enjoy doing most is implementing a feature using as little code as possible. You can do this in JavaScript by chaining methods. For example, when a user logs into your application, you want to change the status of the user to 'online', and when they log out, you change it back to 'offline':

class Twita {
  constructor(username, offlineStatus) {
    this.username = username;
    this.offlineStatus = offlineStatus;
  }
  login() {
    console.log(`${this.username} is logged in`);
    return this;
  }
  setOnlineStatus() {
    // set the online status to true
    this.offlineStatus = false;
    console.log(`${this.username} is online`);
    return this;
  }
  setOfflineStatus() {
    // set the offline status to true
    this.offlineStatus = true;
    console.log(`${this.username} is offline`);
    return this;
  }
  logout() {
    console.log(`${this.username} is logged out`);
    return this;
  }
}

The code above declares a Twita class with username and offlineStatus properties and login, logout, setOnlineStatus, and setOfflineStatus methods. To chain the methods, you can use the dot notation:

const user = new Twita("Adeleye", true);
twita.login().setOnlineStatus().logout().setOfflineStatus();

The code above will run all the functions sequentially on the user object and return a response:

Method chain result

The image shows the result of the method chain.

Note: You need to return the current object by returning this at the end of each function.

Next, let's look at how to build on existing classes through inheritance.

Class Inheritance

When working with objects, you'll most likely run into situations where you need to create an object that is really similar to an object that already exists in your code, but you know they can't be the same. For example, when building an e-commerce application, you will have a Product class with properties like name, price, description, and image and methods like formatPrice and addToCart.

However, what if you have multiple types of products with different specifications:

  • Books with author, weight, and page details.
  • Furniture with length, width, height, and wood-type details.
  • Movies CDs with size, duration, and content details.

In this case, creating a class for each product will result in a lot of code duplication, which is one of the major rules of OOP and programming in general - don't repeat yourself (DRY).

Class inheritance allows you to create objects based on other objects. For example, you can solve the problem mentioned above by creating a Product class:

class Product {
  constructor(name, price, description, image) {
    this.name = name;
    this.price = price;
    this.description = description;
    this.image = image;
  }
  formatprice() {
    return `${this.price}$`;
  }
  addToCart() {
    cart.add(this);
  }
}

Then, create a subclass for each product type using the extends keyword:

class Book extends Product {
  constructor(name, price, description, image, weight, pages, author) {
    super(name, price, description, image);
    this.author = author;
    this.weight = weight;
    this.pages = pages;
  }
}

class Movie extends Product {
  constructor(
    name,
    price,
    description,
    image,
    size,
    contentDescription,
    duration
  ) {
    super(name, price, description, image);
    this.size = size;
    this.contentDescription = contentDescription;
    this.duration = duration;
  }
}

class Furniture extends Product {
  constructor(
    name,
    price,
    description,
    image,
    woodType,
    length,
    height,
    width
  ) {
    super(name, price, description, image);
    this.woodType = woodType;
    this.length = length;
    this.height = height;
    this.width = width;
  }
}

The code above declares the Book, Movie, and Furniture product types by extending the Product class.

There are two new keywords in the above code: extends and super. Let's look at them in the next sections.

The extends Keyword

The extends keyword is self-explanatory; it is used to extend the capability of another class. In our case, we used it to create the Book, Movie, and Furniture classes by extending the Product class.

The super Keyword

The super keyword eliminates the multiple declarations you would otherwise need to repeat for each new child class. For example, the super function called in the above code replaced the following code:

    this.name = name;
    this.price = price;
    this.description = description;
    this.image = image;

Remember DRY? the reason for this is not to repeat the above code because it has been written inside the Product class.

The super function can be omitted if the child class does not need a constructor:

class Animal {
  constructor(name, species, color) {
    this.name = name;
    this.species = species;
    this.color = color;
  }
  makeSound() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Bird extends Animal {
  fly() {
    console.log(`${this.name} flies.`);
  }
}

The code above declares a parent Animal class and a child Bird class that does not need a constructor to work because it is not declaring any new variable inside the constructor. Therefore, the following code should work:

const bird = new Bird('Chloe', 'Parrot', 'Green'); 
console.log(`${bird.name} is a ${bird.color} ${bird.species}`);

The code above will work even though there's nothing like name, color, or species inside the Bird class:

Child class without constructor and super result

You don't need to call super or repeat the same constructor if you only add methods to the child class.

Conclusion

In this tutorial, you learned about object-oriented programming and classes in JavaScript and how it helps to keep your code clean, DRY, and reusable. We covered the four core concepts of object-oriented programming, including abstraction, encapsulation, inheritance, and polymorphism.

You also learned that using object-oriented programming paradigms and classes in your code has a lot of advantages, from improving application structure and code reusability to reducing code complexity.

Whoa! That was a long ride. Thanks for reading.

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Adebayo Adams

    Being a self taught developer himself, Adams understands that consistent learning and doing is the way to become self sufficient as a software developer. He loves learning new things and firmly believes that learning never stops.

    More articles by Adebayo Adams
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Start free trial
    Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
    Start free trial
    “Wow — Customers are blown away that I email them so quickly after an error.”
    Chris Patton, Founder of Punchpass.com
    Start free trial