Understanding Object-Oriented Programming (OOP) in JavaScript


In the world of programming, Object-Oriented Programming (OOP) is a popular paradigm that allows developers to create modular and reusable code. JavaScript, a versatile and widely used programming language, supports OOP concepts, making it a crucial skill for aspiring JavaScript professionals.

According to authentic resources, JavaScript remains one of the most popular languages among developers and one of the most sought-after skills by businesses – given that it is used by 98.6% of all websites. This blog will explore the key OOP concepts in JavaScript and how they can be utilized to write efficient and maintainable code.

Table of Contents

  1. What are OOP Concepts in JavaScript?

  2. Objects and Classes in JavaScript

  3. The Four Pillars of OOP

    • Encapsulation

    • Inheritance

    • Polymorphism

    • Abstraction

  4. Other OOP Concepts in JavaScript

  5. Conclusion

What are OOP Concepts in JavaScript?

Object-Oriented Programming (OOP) is a programming paradigm that organizes code into reusable, self-contained objects. It focuses on objects, which represent real-world entities with their own attributes (data) and behaviors (methods). OOP allows developers to write modular, maintainable, and scalable code by promoting code reusability and encapsulation.

Objects and Classes in JavaScript

Before delving into the main OOP concepts in JavaScript with examples, let’s explore what objects and classes are.

Objects in JavaScript

In JavaScript, objects are crucial for creating complex data structures and modeling real-world concepts. They are collections of key-value pairs, where the keys are known as properties and the values can be of any data type, including other objects or functions.

Creating Objects

JavaScript offers multiple ways to create objects. One common method is using object literals, which allow you to define an object and its properties concisely.

const person = {
  name: "John",
  age: 25,
  profession: "Engineer"
};

In this example, person is an object with properties such as name, age, and profession. Each property has a corresponding value.

Constructors and Prototypes

JavaScript also provides constructors and prototypes as mechanisms for creating objects and defining shared properties and methods. Constructors are functions used to create instances of objects. Prototypes enable objects to inherit properties and methods from their prototype objects.

// Constructor function
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// Creating an object instance using the constructor
const john = new Person("John", 25);

In this example, the Person function acts as a constructor for creating Person objects. The new keyword is used to instantiate an object from the constructor, passing the necessary arguments.

Object Instances

Object instances are individual objects created using constructors. Each instance has its own set of properties and values while also sharing the same methods defined in the prototype. This allows for code reusability and efficient memory usage.

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log("Hello, my name is " + this.name);
};

const john = new Person("John", 25);
const jane = new Person("Jane", 30);

john.greet(); // Output: Hello, my name is John
jane.greet(); // Output: Hello, my name is Jane

In this example, the greet method is defined in the prototype of the Person constructor. Both john and jane instances can access and invoke the greet method, even though it is defined only once in the prototype.

Accessing Object Properties and Methods

Accessing object properties and invoking methods can be done using the dot notation or the bracket notation. The dot notation is typically used when the property name is known in advance, while the bracket notation allows for dynamic property access.

const person = {
  name: "John",
  age: 25,
  greet: function() {
    console.log("Hello, I'm " + this.name);
  }
};

console.log(person.name); // Output: John
console.log(person["age"]); // Output: 25
person.greet(); // Output: Hello, I'm John

In this example, the properties name and age are accessed using the dot notation, while the greet method is invoked using parentheses.

Objects are integral to JavaScript, providing a versatile and powerful way to structure and manipulate data. By understanding how to create objects, utilize constructors and prototypes, and access properties and methods, you can leverage the full potential of objects in JavaScript programming.

Classes in JavaScript

Now that we know what an object is, let's define a class. In JavaScript, a class is a blueprint for creating objects with shared properties and methods. It provides a structured way to define and create multiple instances of similar objects. Classes in JavaScript follow the syntax introduced in ECMAScript 2015 (ES6) and offer a more organized approach to object-oriented programming.

When creating a class in JavaScript, you use the class keyword followed by the name of the class. The class can have a constructor method, which is a special method that is called when creating a new instance of the class. The constructor is used to initialize the object's properties.

Within a class, you can define methods that define the behavior of the class instances. These methods can be accessed and invoked by the instances of the class. You can also define static methods that are associated with the class itself rather than its instances. Here's an example of a JavaScript class:

class Car {
  constructor(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
  }

  getAge() {
    const currentYear = new Date().getFullYear();
    return currentYear - this.year;
  }

  static isOld(car) {
    return car.getAge() >= 10;
  }
}

const myCar = new Car("Toyota", "Camry", 2018);
console.log(myCar.getAge()); // Output: 6 (assuming the current year is 2024)
console.log(Car.isOld(myCar)); // Output: false

In this example, the Car class has a constructor that takes the make, model, and year as parameters and initializes the respective properties. It also has a getAge() method that calculates the age of the car based on the current year. The isOld() method is a static method that determines if a car instance is considered old.

Explanation of Static Methods

Static methods are called on the class itself, not on instances of the class. They are used to perform actions that are relevant to the class as a whole, not to any specific instance. In the example above, Car.isOld(myCar) is a static method call that determines if the car is old based on the class's logic.

Real-Life Example: Library System

Consider a library system where we have books and members. We can create classes to represent these entities.

class Book {
  constructor(title, author) {
    this.title = title;
    this.author = author;
  }
}

class Member {
  constructor(name, memberId) {
    this.name = name;
    this.memberId = memberId;
    this.borrowedBooks = [];
  }

  borrowBook(book) {
    this.borrowedBooks.push(book);
    console.log(`${this.name} borrowed "${book.title}" by ${book.author}`);
  }
}

const book1 = new Book("1984", "George Orwell");
const book2 = new Book("To Kill a Mockingbird", "Harper Lee");

const member = new Member("Alice", 123);
member.borrowBook(book1); // Output: Alice borrowed "1984" by George Orwell
member.borrowBook(book2); // Output: Alice borrowed "To Kill a Mockingbird" by Harper Lee

In this example:

  • We have a Book class with properties for title and author.

  • We have a Member class with properties for name, memberId, and borrowedBooks. The borrowBook method allows a member to borrow a book and adds it to the borrowedBooks array.

The Four Pillars of OOP

This section of the blog will expand on the four pillars of OOP with real-life examples and code snippets.

Encapsulation

Encapsulation is defined as the process of bundling data and methods together within a single unit, known as an object. It allows for the organization of related data and operations, promoting code modularity and reusability. Encapsulation provides two key benefits: data hiding and access control.

By encapsulating data, an object's internal state is hidden from external entities. This ensures that the data can only be accessed and modified through defined methods, preventing direct manipulation and maintaining data integrity. Access control mechanisms, such as private and public modifiers, allow developers to control the visibility and accessibility of object members.

Real-Life Example of Encapsulation

Consider a bank account system where the balance should not be directly accessible from outside the account object.

class BankAccount {
  #balance; // Private property

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      console.log(`Deposited: $${amount}`);
    } else

 {
      console.log("Invalid deposit amount");
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      console.log(`Withdrawn: $${amount}`);
    } else {
      console.log("Invalid withdrawal amount");
    }
  }

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

const myAccount = new BankAccount(1000);
myAccount.deposit(500); // Output: Deposited: $500
myAccount.withdraw(200); // Output: Withdrawn: $200
console.log(myAccount.getBalance()); // Output: 1300

In this example:

  • The BankAccount class encapsulates the balance property, making it private using the # syntax.

  • The deposit and withdraw methods provide controlled access to modify the balance.

  • The getBalance method allows read-only access to the balance.

Inheritance

Inheritance is defined as the process of creating new classes, known as subclasses or derived classes, based on existing classes, known as superclasses or base classes. The subclasses inherit the properties and methods of the superclass, allowing code reuse and promoting a hierarchical relationship between classes.

Inheritance enables the creation of specialized subclasses that inherit the common attributes and behaviors from the superclass. Subclasses can also add their own unique properties and methods or override the inherited ones. This mechanism allows for the creation of a class hierarchy, promoting modular and extensible code.

Real-Life Example of Inheritance

Consider a company where we have a general Employee class and specific types of employees like Developer and Designer.

class Employee {
  constructor(name, salary) {
    this.name = name;
    this.salary = salary;
  }

  work() {
    console.log(`${this.name} is working.`);
  }
}

class Developer extends Employee {
  constructor(name, salary, language) {
    super(name, salary);
    this.language = language;
  }

  code() {
    console.log(`${this.name} is coding in ${this.language}.`);
  }
}

class Designer extends Employee {
  constructor(name, salary, tool) {
    super(name, salary);
    this.tool = tool;
  }

  design() {
    console.log(`${this.name} is designing using ${this.tool}.`);
  }
}

const dev = new Developer("Alice", 80000, "JavaScript");
const des = new Designer("Bob", 70000, "Photoshop");

dev.work(); // Output: Alice is working.
dev.code(); // Output: Alice is coding in JavaScript.
des.work(); // Output: Bob is working.
des.design(); // Output: Bob is designing using Photoshop.

In this example:

  • The Employee class serves as the base class with common properties and methods.

  • The Developer and Designer classes inherit from Employee and add their specific properties and methods.

Polymorphism

Polymorphism is a concept in OOP that allows objects of different classes to be treated as objects of a common superclass. It means "many shapes" and lets one interface be used for different data types. This allows functions to use objects of different types at different times.

Real-Life Example of Polymorphism

Consider a company where employees perform different roles. We can have a superclass called Employee with a method called work(). Different types of employees (subclasses) such as Developer and Designer will have their own implementations of the work() method.

  • Developer: Codes software.

  • Designer: Creates designs.

Example of Polymorphism in JavaScript

Let's see how we can implement this example in JavaScript.

class Employee {
  constructor(name) {
    this.name = name;
  }

  work() {
    console.log(`${this.name} is working.`);
  }
}

class Developer extends Employee {
  work() {
    console.log(`${this.name} is coding.`);
  }
}

class Designer extends Employee {
  work() {
    console.log(`${this.name} is designing.`);
  }
}

const employees = [new Developer("Alice"), new Designer("Bob")];

employees.forEach(employee => {
  employee.work();
});

In this example:

  • The Employee class has a method called work().

  • The Developer and Designer subclasses override the work() method with their specific implementations.

  • When we call the work() method on each employee, it executes the overridden method in the respective subclass.

Abstraction

Abstraction is defined as the process of hiding the complex implementation details of an object and exposing only the essential features to the users. It allows for the creation of simplified and user-friendly interfaces that focus on what an object does rather than how it does it. Abstraction promotes code clarity, maintainability, and reusability.

By abstracting away unnecessary details, developers can interact with objects at a higher level of abstraction, focusing on their functionalities and interactions. This simplifies the usage of complex systems and reduces the cognitive load on developers, making the codebase more manageable.

Real-Life Example of Abstraction

Consider a mobile phone. A normal user doesn't need to know the intricate details of how the phone's hardware and software work together. They only interact with the phone through its user interface.

Example of Abstraction in JavaScript

class Phone {
  constructor(model) {
    this.model = model;
  }

  makeCall(number) {
    console.log(`Calling ${number} from ${this.model}...`);
  }

  sendMessage(number, message) {
    console.log(`Sending message to ${number}: ${message}`);
  }
}

const myPhone = new Phone("iPhone 12");
myPhone.makeCall("123-456-7890"); // Output: Calling 123-456-7890 from iPhone 12...
myPhone.sendMessage("123-456-7890", "Hello!"); // Output: Sending message to 123-456-7890: Hello!

In this example:

  • The Phone class abstracts the complexity of making calls and sending messages.

  • The user interacts with simple methods like makeCall and sendMessage without worrying about the underlying implementation.

Other OOP Concepts in JavaScript

In addition to the four pillars of OOP, there are other important concepts in JavaScript that are related to object-oriented programming. These concepts enhance the functionality and flexibility of JavaScript applications. Let's explore some of these concepts with real-life examples and code snippets.

Closures

A closure is a function that retains access to its lexical scope, even when the function is executed outside that scope. Closures are often used to create private variables and functions, allowing for data encapsulation and maintaining state.

Real-Life Example of Closures

Consider a counter function that keeps track of the count and allows incrementing it.

function createCounter() {
  let count = 0;

  return {
    increment() {
      count++;
      console.log(`Count: ${count}`);
    },
    decrement() {
      count--;
      console.log(`Count: ${count}`);
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment(); // Output: Count: 1
counter.increment(); // Output: Count: 2
counter.decrement(); // Output: Count: 1
console.log(counter.getCount()); // Output: 1

In this example:

  • The createCounter function returns an object with methods to increment, decrement, and get the count.

  • The count variable is private and can only be accessed through the methods provided.

Immediately Invoked Function Expressions (IIFE)

An IIFE is a function that is executed immediately after it is defined. It is often used to create a private scope and avoid polluting the global namespace.

Real-Life Example of IIFE

Consider a scenario where you want to create a module with private variables and methods.

const myModule = (function() {
  let privateVariable = "I am private";

  function privateMethod() {
    console.log(privateVariable);
  }

  return {
    publicMethod() {
      privateMethod();
    }
  };
})();

myModule.publicMethod(); // Output: I am private

In this example:

  • The myModule object is created using an IIFE.

  • The privateVariable and privateMethod are encapsulated within the IIFE and cannot be accessed directly from outside.

Modules

Modules in JavaScript allow you to organize and encapsulate code into separate files, promoting code reusability and maintainability. Modules can export and import functionality, making it easier to manage large codebases.

Real-Life Example of Modules

Consider a scenario where you want to create a utility module with common functions.

// mathUtils.js (Module file)
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// main.js (Importing module)
import { add, subtract } from './mathUtils.js';

console.log(add(5, 3)); // Output: 8
console.log(subtract(5, 3)); // Output: 2

In this example:

  • The mathUtils.js file defines and exports add and subtract functions.

  • The main.js file imports and uses these functions, demonstrating modularity and code reuse.

Conclusion

Object-Oriented Programming (OOP) in JavaScript provides powerful concepts and techniques for writing modular, maintainable, and reusable code. By understanding and utilizing objects, classes, encapsulation, inheritance, polymorphism, abstraction, and other OOP-related concepts like closures, IIFE, and modules, developers can create robust and scalable applications.

With the knowledge gained from this blog, you can start applying these OOP principles in your JavaScript projects, improving the organization and quality of your code. Embrace the power of OOP in JavaScript and unlock new possibilities for building sophisticated and efficient applications.