Object-Oriented Programming in JavaScript

⏳ 13 min read
📝 1776 words
Object-Oriented Programming in JavaScript

Introduction to Object-Oriented Programming (OOP) in JavaScript

Four Pillars of OOP

Object-oriented programming (OOP) is built around four main principles, often referred to as the "four pillars" of OOP. These principles help in designing and organizing code in a way that is modular, reusable, and easier to maintain. The four pillars are:

  1. Encapsulation: This principle involves bundling the data (attributes) and methods (functions) that operate on the data into a single unit known as a class. Encapsulation helps in restricting direct access to some of an object's components, which can prevent the accidental modification of data. It promotes data hiding and protects the integrity of the object's state.

  2. Abstraction: Abstraction is the concept of simplifying complex systems by modeling classes based on the essential properties and behaviors an object should have, while ignoring irrelevant details. It allows programmers to focus on interactions at a higher level without needing to understand all the underlying complexities.

  3. Inheritance: Inheritance is a mechanism that allows a new class (called a subclass or derived class) to inherit properties and behaviors (methods) from an existing class (called a superclass or base class). This promotes code reusability and establishes a hierarchical relationship between classes.

  4. Polymorphism: Polymorphism allows objects of different classes to be treated as objects of a common superclass. It enables a single interface to represent different underlying forms (data types). The most common use of polymorphism is when a method can perform different functions based on the object that it is acting upon, allowing for flexibility and the ability to extend code without modifying existing functionality.

Benefits of OOP

Object-oriented programming offers several advantages that make it a popular choice for software development:

  • Modularity: OOP promotes modularity by organizing code into classes and objects. This makes it easier to manage and understand complex systems, as each class can be developed and tested independently.

  • Reusability: Classes and objects can be reused across different parts of a program or in different projects, reducing redundancy and speeding up development time.

  • Maintainability: OOP makes it easier to maintain and update code. Changes made to a class can automatically propagate to all instances of that class, reducing the risk of errors and inconsistencies.

  • Flexibility: OOP allows for the creation of flexible and extensible code. New classes can be created based on existing ones, and polymorphism enables the addition of new functionality without altering existing code.

  • Real-world modeling: OOP provides a natural way to model real-world entities and their interactions, making it easier to design systems that reflect real-world scenarios. Overall, object-oriented programming is a powerful paradigm that helps developers create organized, reusable, and maintainable code, making it a preferred choice for many software development projects.

Pillars

Understanding these four pillars of OOP is crucial for anyone looking to master object-oriented programming in JavaScript or any other programming language that supports OOP concepts. By applying these principles effectively, developers can create robust and scalable applications.

Encapsulation

Using Encapsulation, we group related variables and functions together, and this way we can reduce complexity, now we can use this objects in different parts of the programs or in different programs.

Car → make, model, color (properties) and (methods) like start, stop and move.

So in OOP we group related variable s and functions that operate on them into objects, And this is what we call encapsulation. Let me show you an example :

class Car {
    constructor(make, model, color) {
        this.make = make;
        this.model = model;
        this.color = color;
    }
    start() {
        console.log(`The ${this.color} ${this.make} ${this.model} is starting.`);
    }
    stop() {
        console.log(`The ${this.color} ${this.make} ${this.model} is stopping
.`);

    }
}
const myCar = new Car('Toyota', 'Corolla', 'blue');
myCar.start(); // The blue Toyota Corolla is starting.
myCar.stop(); // The blue Toyota Corolla is stopping.

Example of Procedural programming:

let baseSalary = 30_000;
let overtime = 10;
let rate = 20;

function getWage(baseSalary, overtitme, rate) {
  return baseSalary + (overtime * rate);
}

Example of OOP:

class Employee {
  constructor(baseSalary, overtime, rate) {
    this.baseSalary = baseSalary;
    this.overtime = overtime;
    this.rate = rate;
  }
    getWage() {
        return this.baseSalary + (this.overtime * this.rate);
    }
}
const employee = new Employee(30_000, 10, 20);
console.log(employee.getWage());

The best functions are those with no parameters - Uncle Bob - Robert C Martin

The fewer the number of parameters, its easier to use and maintain that function. So that's encapsulation.

Abstraction

Abstraction is a way to hide complexity from users. It allows us to create an interface for the object that is easy to use and understand.

With Abstraction, we hide the details and the complexity and show only the essentials, and also isolates the impact of changes in the code.

think of a DVD player as an object. This DVD player has a complex logic board in the inside and few buttons on the outside to interact with. You simply press the play button and you don't care what happens on the inside. All the complexity is hidden from you. This is abstraction in practice.

We can use the same technique in our objects.

So we can hide some of the properties and methods from the outside and this gives us couple of benefits :

  • Simpler Interface
  • Reduce the Impact of Change - Let's imagine that tommorow we change these inner or private methods

eg.

class BankAccount {
    #accountNumber;
    #balance;
    constructor(accountNumber, initialBalance) {
        this.#accountNumber = accountNumber;
        this.#balance = initialBalance;
    }
    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            console.log(`Deposited: $${amount}`);
        } else {
            console.log('Deposit amount must be positive.');
        }
    }
    withdraw(amount) {
        if (amount > 0 && amount <= this.#balance) {
            this.#balance -= amount;
            console.log(`Withdrew: $${amount}`);
        } else {
            console.log('Invalid withdrawal amount.');
        }
    }
    getBalance() {
        return this.#balance;
    }
}
const myAccount = new BankAccount('123456789', 1000);
myAccount.deposit(500); // Deposited: $500
myAccount.withdraw(200); // Withdrew: $200
console.log(`Current Balance: $${myAccount.getBalance()}`); // Current Balance:
$1300

In this example, the accountNumber and balance properties are private and cannot be accessed directly from outside the class. The user can only interact with the account through the public methods deposit, withdraw, and getBalance. This simplifies the interface and reduces the impact of changes to the internal implementation of the class.

Inheritance

Inheritance is a mechanism that allows you to eliminate redundant code

Examples

Elements like TextBox, Select and Checkbox, they have all some things in common like they have properties like hidden, innerHTML and methods like click() and focus(). Instead of redefining all these properties and methods for each HTML element, we can define them once in a generic object and call it HTMLElement and have other objects inherit this properties and methods, so inheritance helps us eliminate redundant code.

class HTMLElement
{
    constructor(hidden, innerHTML) {
        this.hidden = hidden;
        this.innerHTML = innerHTML;
    }
    click() {
        console.log('Element clicked');
    }
    focus() {
        console.log('Element focused');
    }
}   
class TextBox extends HTMLElement
{
    constructor(hidden, innerHTML, maxLength) {
        super(hidden, innerHTML);
        this.maxLength = maxLength;
    }
}
class Select extends HTMLElement
{
    constructor(hidden, innerHTML, options) {
        super(hidden, innerHTML);
        this.options = options;
    }
}
class Checkbox extends HTMLElement
{
    constructor(hidden, innerHTML, isChecked) {
        super(hidden, innerHTML);
        this.isChecked = isChecked;
    }
}

In this example, we have a base class HTMLElement that defines common properties and methods for HTML elements. The TextBox, Select, and Checkbox classes inherit from HTMLElement, allowing them to reuse the code defined in the base class while also adding their own specific properties.

Polymorphism

Polymorphism allows us to use a single interface to represent different underlying forms (data types). The most common use of polymorphism is when a method can perform different functions based on the object that it is acting upon.

Poly means many, and morph means Form, so polymorphism means many forms.

In object oriented programming, polymorphism is a technique that allows you to get rid of long if else or switch and case statements.

In earlier example, the way each element is rendered is different from the others. If you want to render multiple HTML elements in a procedural way, Our code would probably look like this :

function renderElement(element) {
    if (element.type === 'TextBox') {
        // Render TextBox
    } else if (element.type === 'Select') {
        // Render Select
    } else if (element.type === 'Checkbox') {
        // Render Checkbox
    }
}

With polymorphism, we can eliminate these conditionals by defining a common method in the base class and overriding it in each derived class.

class HTMLElement
{
    constructor(hidden, innerHTML) {
        this.hidden = hidden;
        this.innerHTML = innerHTML;
    }
    render() { // Common render method
        console.log('Rendering HTMLElement');
    }
}
class TextBox extends HTMLElement
{
    constructor(hidden, innerHTML, maxLength) {
        super(hidden, innerHTML);
        this.maxLength = maxLength;
    }
    render() { // Overriding render method
        console.log('Rendering TextBox');
    }
}
class Select extends HTMLElement
{
    constructor(hidden, innerHTML, options) {
        super(hidden, innerHTML);
        this.options = options;
    }
    render() { // Overriding render method
        console.log('Rendering Select');
    }
}
class Checkbox extends HTMLElement
{
    constructor(hidden, innerHTML, isChecked) {
        super(hidden, innerHTML);
        this.isChecked = isChecked;
    }
    render() {  // Overriding render method
        console.log('Rendering Checkbox');
    }
}
const elements = [
    new TextBox(false, 'Enter text', 100),
    new Select(false, 'Choose option', ['Option 1', 'Option 2']),
    new Checkbox(false, 'Accept terms', true)
];
for (let element of elements) {
    element.render(); // Calls the appropriate render method based on the object type
}

and the render method will behave differently depending on the type of the object we are referencing.

So we can get rid of the switch case and write the code like this:

element.render()

This is polymorphism in action. It allows us to write cleaner and more maintainable code by leveraging method overriding and dynamic method dispatch.

Summary

Object-oriented programming is a powerful programming paradigm that helps developers create organized, reusable, and maintainable code. By understanding and applying the four pillars of OOP—Encapsulation, Abstraction, Inheritance, and Polymorphism—developers can design robust and scalable applications. Whether you're building small scripts or large-scale software systems, OOP principles can significantly enhance your coding practices and improve the overall quality of your code.

  • Encapsulation helps in bundling data and methods together, promoting data hiding and protecting the integrity of an object's state.
  • Abstraction simplifies complex systems by modeling classes based on essential properties and behaviors, allowing developers to focus on higher-level interactions.
  • Inheritance promotes code reusability by allowing new classes to inherit properties and behaviors from existing classes, establishing hierarchical relationships.
  • Polymorphism enables a single interface to represent different underlying forms, allowing for flexibility and extensibility in code without modifying existing functionality.

By mastering these concepts, developers can create more efficient, flexible, and maintainable code, making OOP a preferred choice for many software development projects.