Learn How to Implement Object-Oriented Programming in JavaScript for Real Projects

Muhaymin Bin Mehmood

Muhaymin Bin Mehmood

· 9 min read
Learn How to Implement Object-Oriented Programming in JavaScript for Real Projects Banner Image
Learn How to Implement Object-Oriented Programming in JavaScript for Real Projects Banner Image

JavaScript has long been known as a flexible and dynamic language, but its ability to embrace Object-Oriented Programming (OOP) concepts makes it even more powerful. Whether you’re developing web applications, games, or any software, understanding OOP in JavaScript will improve your code structure, reusability, and efficiency.

In this blog, we’ll explore the fundamentals of OOP in JavaScript with practical examples, including real-world applications, to help you understand how to apply these concepts effectively.

Table of Contents:

  1. What is Object-Oriented Programming (OOP)?
  2. First-Class Citizens in JavaScript
  3. Creating First Instances
  4. Inheritance: Reusing Code
  5. Encapsulation: Keeping It Private
  6. Polymorphism: Flexibility in Code
  7. Abstract Classes: Creating Blueprints

1. What is Object-Oriented Programming (OOP)?

OOP is a programming paradigm centered around the concept of "objects." An object is a collection of data (properties) and methods (functions) that work on that data. In JavaScript, OOP allows developers to model real-world entities like users, products, or transactions by representing them as objects.

Real-World Example: Let’s say we’re building a simple e-commerce website. Each product in the store can be represented as an object with properties like name, price, and methods like applyDiscount().

let product = {
    name: 'Laptop',
    price: 1000,
    applyDiscount: function(discount) {
        this.price -= discount;
        return this.price;
    }
};

console.log(product.applyDiscount(100)); // Output: 900

In this scenario, each product in the online store would be modeled as an object, and you could apply discounts, calculate taxes, etc., using OOP.

2. First-Class Citizens in JavaScript

In JavaScript, functions are treated as "first-class citizens," meaning they can be assigned to variables, passed as arguments, and returned from other functions. This is crucial when you want objects to not only store data but also perform actions related to that data.

Real-World Example: Imagine an online ticket booking system where each event is an object. A function to calculate the total price based on the number of tickets would be a method of the object.

const event = {
    name: 'Concert',
    pricePerTicket: 50,
    calculateTotalPrice: function(numTickets) {
        return numTickets * this.pricePerTicket;
    }
};

console.log(event.calculateTotalPrice(4)); // Output: 200

In this example, the calculateTotalPrice function is a method tied to the event object.

3. Creating First Instances

The new keyword in JavaScript allows you to create instances of a class, making it easy to reuse the same structure for multiple objects. This concept is particularly useful when you have similar objects like multiple users, products, or transactions.

Real-World Example: In an online food delivery app, each order can be an instance of a class Order. Each order would have a customer name, total cost, and delivery address.

class Order {
    constructor(customerName, totalCost, deliveryAddress) {
        this.customerName = customerName;
        this.totalCost = totalCost;
        this.deliveryAddress = deliveryAddress;
    }

    confirmOrder() {
        return `Order for ${this.customerName} confirmed! Total cost: $${this.totalCost}`;
    }
}

let order1 = new Order('John Doe', 35, '123 Main St');
console.log(order1.confirmOrder()); 
// Output: "Order for John Doe confirmed! Total cost: $35"

This allows the system to create and manage multiple customer orders easily.

4. Inheritance: Reusing Code

Inheritance is one of the most powerful OOP features. It allows a class to inherit properties and methods from another class, reducing code duplication and improving maintainability.

Real-World Example: In a job portal system, you might have a Job class with properties like title and description, and specific job types like FullTimeJob and PartTimeJob that inherit from Job.

class Job {
    constructor(title, description) {
        this.title = title;
        this.description = description;
    }
}

class FullTimeJob extends Job {
    constructor(title, description, salary) {
        super(title, description);
        this.salary = salary;
    }
}

let developerJob = new FullTimeJob('JavaScript Developer', 'Develop awesome JS apps', 80000);
console.log(developerJob.title); // Output: JavaScript Developer
console.log(developerJob.salary); // Output: 80000

Here, the FullTimeJob inherits properties from Job and adds additional properties like salary.

5. Encapsulation: Keeping It Private

Encapsulation is all about keeping data safe from unintended interference. In JavaScript, you can keep certain properties private using the # symbol, which ensures that they cannot be accessed or modified directly outside of the class.

Real-World Example: In a banking application, customer account details like the balance should be private and not directly accessible or modifiable.

class BankAccount {
    #balance;

    constructor(owner, balance) {
        this.owner = owner;
        this.#balance = balance;
    }

    deposit(amount) {
        this.#balance += amount;
        return this.#balance;
    }

    checkBalance() {
        return this.#balance;
    }
}

let account = new BankAccount('Alice', 500);
account.deposit(200);
console.log(account.checkBalance()); // Output: 700
console.log(account.#balance); // Error: Private field '#balance' must be declared in an enclosing class

This encapsulation ensures that a user's balance is safely managed and cannot be modified directly.

6. Polymorphism: Flexibility in Code

Polymorphism refers to the ability of different objects to respond to the same method call in different ways. This feature provides flexibility when designing systems with interchangeable components.

Real-World Example: In a ride-sharing app, you might have different vehicle types (e.g., Car, Bike) that all have a calculateFare() method, but the logic for calculating the fare is different for each type.

class Vehicle {
    calculateFare(distance) {
        return 0; // Base fare, overridden by subclasses
    }
}

class Car extends Vehicle {
    calculateFare(distance) {
        return distance * 2; // $2 per km
    }
}

class Bike extends Vehicle {
    calculateFare(distance) {
        return distance * 1.5; // $1.5 per km
    }
}

let carRide = new Car();
let bikeRide = new Bike();

console.log(carRide.calculateFare(10)); // Output: 20
console.log(bikeRide.calculateFare(10)); // Output: 15

Both Car and Bike implement the calculateFare method, but their implementations differ based on the vehicle type.

7. Abstract Classes: Creating Blueprints

Abstract classes serve as templates for other classes, outlining required methods that must be implemented. While JavaScript doesn’t have formal abstract classes like other languages, you can simulate this behavior.

Real-World Example: In a payment system, you might have different types of payment methods (e.g., CreditCard, PayPal). Each method would have a processPayment() function, but the way payment is processed will differ for each.

class PaymentMethod {
    processPayment() {
        throw new Error('Method processPayment() must be implemented.');
    }
}

class CreditCardPayment extends PaymentMethod {
    processPayment(amount) {
        console.log(`Processing $${amount} payment via Credit Card`);
    }
}

class PayPalPayment extends PaymentMethod {
    processPayment(amount) {
        console.log(`Processing $${amount} payment via PayPal`);
    }
}

let payment1 = new CreditCardPayment();
payment1.processPayment(100); // Output: Processing $100 payment via Credit Card

let payment2 = new PayPalPayment();
payment2.processPayment(50); // Output: Processing $50 payment via PayPal

This design ensures that each payment method adheres to the required behavior while implementing its own logic.

Real-World Combination Example: E-commerce Platform

Let’s combine all the OOP concepts into a larger real-world example where we manage an online e-commerce system. This system will handle different types of users, orders, and payment methods, encapsulating their properties and actions, while inheriting common functionalities and demonstrating polymorphism in payment processing.

Step 1: Defining the Base User Class with Encapsulation

We'll start by defining a base User class that stores common information like name, email, and password. The password property will be encapsulated (made private), ensuring it can't be directly accessed or modified from outside the class. We'll also add a login() method that checks if the entered password matches the stored one.

class User {
    #password; // Encapsulation

    constructor(name, email, password) {
        this.name = name;
        this.email = email;
        this.#password = password; 
    }

    login(enteredPassword) {
        return this.#password === enteredPassword 
            ? `${this.name} successfully logged in!`
            : `Incorrect password for ${this.name}`;
    }
}

Step 2: Inheriting from the User Class

Now, we’ll create two specific types of users: Customer and Admin. Both will inherit from the User class but will have different methods relevant to their role. For example, a Customer will be able to place orders, while an Admin will manage the platform.

class Customer extends User {
    constructor(name, email, password, cart = []) {
        super(name, email, password);
        this.cart = cart; // Unique property for customers
    }

    placeOrder() {
        if (this.cart.length > 0) {
            return `${this.name} placed an order for ${this.cart.length} items.`;
        } else {
            return `Cart is empty. Add items to the cart to place an order.`;
        }
    }
}

class Admin extends User {
    constructor(name, email, password) {
        super(name, email, password);
    }

    managePlatform() {
        return `${this.name} is managing the platform settings.`;
    }
}

Step 3: Using Polymorphism for Payment Processing

We can now introduce multiple payment methods—such as CreditCardPayment and PayPalPayment. Both will have the same method processPayment(), but their implementation will differ based on the payment method (polymorphism).

class PaymentMethod {
    processPayment() {
        throw new Error('Method processPayment() must be implemented.');
    }
}

class CreditCardPayment extends PaymentMethod {
    processPayment(amount) {
        return `Processing credit card payment of $${amount}`;
    }
}

class PayPalPayment extends PaymentMethod {
    processPayment(amount) {
        return `Processing PayPal payment of $${amount}`;
    }
}

Step 4: Bringing It All Together

Finally, we can demonstrate how all these classes work together in an e-commerce system. We’ll create an instance of a Customer, allow them to log in, add items to their cart, and place an order using polymorphic payment methods.

// Creating a customer and logging in
let customer1 = new Customer('Alice', 'alice@example.com', 'securePass123');
console.log(customer1.login('securePass123')); // Output: Alice successfully logged in!

// Adding items to the cart
customer1.cart.push('Laptop', 'Smartphone');
console.log(customer1.placeOrder()); // Output: Alice placed an order for 2 items.

// Processing payments using different methods (Polymorphism)
let paymentMethod1 = new CreditCardPayment();
let paymentMethod2 = new PayPalPayment();

console.log(paymentMethod1.processPayment(1500)); // Output: Processing credit card payment of $1500
console.log(paymentMethod2.processPayment(1500)); // Output: Processing PayPal payment of $1500

Step 5: Admin Managing the Platform

Let’s also simulate the Admin managing the platform.

let admin1 = new Admin('Bob', 'admin@example.com', 'adminPass321');
console.log(admin1.login('adminPass321')); // Output: Bob successfully logged in!
console.log(admin1.managePlatform()); // Output: Bob is managing the platform settings.

Recap of OOP Principles in the E-commerce Example:

  • Encapsulation: The password property is private and can’t be accessed directly from outside the class.
  • Inheritance: Both Customer and Admin inherit from the User class, reusing common properties and methods.
  • Polymorphism: The processPayment() method behaves differently depending on whether it’s a credit card or PayPal payment.
  • Real-World Application: We've combined all these concepts to model a realistic e-commerce platform where different user roles and payment methods are handled through OOP.

This combined example showcases how OOP principles can be implemented together to build robust, scalable, and maintainable applications. Each concept—encapsulation, inheritance, and polymorphism—plays an integral role in keeping the code organized and reusable, especially for a complex system like an e-commerce platform.

Conclusion

Object-Oriented Programming (OOP) in JavaScript allows developers to write more structured and maintainable code by modeling real-world entities. From creating classes to understanding inheritance, encapsulation, polymorphism, and abstract classes, mastering these OOP concepts will enhance your JavaScript skills and allow you to build more complex applications.

By using OOP, your code can become cleaner, more efficient, and easier to manage, especially in large-scale projects.

Muhaymin Bin Mehmood

About Muhaymin Bin Mehmood

Front-end Developer skilled in the MERN stack, experienced in web and mobile development. Proficient in React.js, Node.js, and Express.js, with a focus on client interactions, sales support, and high-performance applications.

Copyright © 2024 Mbloging. All rights reserved.