DEV Community

Honeybadger Staff for Honeybadger

Posted on • Originally published at honeybadger.io

Object Oriented Programming in JavaScript

This article was originally written by Adebayo Adams on the Honeybadger Developer Blog.

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}`;
    }
} 
Enter fullscreen mode Exit fullscreen mode

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}`;
    }
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`;
  },
};
Enter fullscreen mode Exit fullscreen mode

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

person.name = "Smith Doe";
console.log(person.name); // returns "Smith Doe"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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";
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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`;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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";
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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");
    }
  }
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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++;
  }
Enter fullscreen mode Exit fullscreen mode

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...
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)