Object Oriented Programming in JavaScript

Object Oriented Programming in JavaScript

First of all, a question arises: are there classes in JavaScript? Historically, JavaScript did not have classes. Technically, even now, there are no traditional classes as seen in other programming languages.

However, if you Google it, some articles will say, "Yes, JavaScript has classes," and that this feature was introduced in ECMAScript 2015 (also known as ES6). It’s important to note that JavaScript is primarily a prototype-based language, not an object-oriented or functional-based language. Everything in JavaScript is prototype-based.

Classes in JavaScript are primarily syntactic sugar over existing prototype-based inheritance mechanisms, meaning you don’t miss out on anything.

OOP in JavaScript

Object-Oriented Programming (OOP) is a programming paradigm, a structure or style of writing code (such as procedural, OOP, functional, etc.).

Objects in JavaScript

An object is a collection of properties (variables, constants, or key-value pairs) and methods (functions).

Why Use OOP?

Before OOP, code was becoming messy, and no chunk of the code could be reused. With the introduction of OOP in JavaScript, we can use features of other languages such as Java and C++. We are able to utilize features such as abstraction, encapsulation, inheritance, and polymorphism, which makes code more modular, reusable, and easier to maintain. OOP allows us to create classes and instances similar to Java and C++.

Parts of OOP

  1. Object Literal

  2. Constructor Function

  3. Prototypes

  4. Classes

  5. Instances (new, this)

The 4 Pillars of OOP

  1. Abstraction:

Abstraction is the concept of hiding complex implementation details and showing only the essential features of an object. This helps in reducing programming complexity and effort.

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

    start() {
        console.log(`${this.model} is starting.`);
    }

    stop() {
        console.log(`${this.model} is stopping.`);
    }
}

const myCar = new Car('Toyota', 2020);
myCar.start();  // Output: Toyota is starting.
myCar.stop();   // Output: Toyota is stopping.

Here, the Car class provides a simple interface to start and stop the car without showing the internal workings of how the car actually starts or stops.

  1. Encapsulation:

Encapsulation is the concept of wrapping data and methods that work on the data within a single unit, usually a class. This keeps data safe from outside interference and misuse.

class Person {
    #name;
    #age;

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

    getDetails() {
        return `Name: ${this.#name}, Age: ${this.#age}`;
    }
}

const person = new Person('John', 30);
console.log(person.getDetails());  // Output: Name: John, Age: 30
// console.log(person.#name); // This will throw an error as #name is private

In this example, #name and #age are private fields, and they can only be accessed through the getDetails method.

  1. Inheritance:

Inheritance is the mechanism by which one class (child class) can inherit the properties and methods of another class (parent class). This allows for code reuse and a hierarchical class structure.

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

    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}

class Dog extends Animal {
    speak() {
        console.log(`${this.name} barks.`);
    }
}

const myDog = new Dog('Rex');
myDog.speak();  // Output: Rex barks.

Here, the Dog class inherits from the Animal class and overrides the speak method to provide a specific implementation.

  1. Polymorphism:

Polymorphism means "many shapes" and it allows objects of different classes to be treated as objects of a common superclass. It is achieved through method overriding and method overloading.

class Animal {
    speak() {
        console.log('Animal makes a sound.');
    }
}

class Dog extends Animal {
    speak() {
        console.log('Dog barks.');
    }
}

class Cat extends Animal {
    speak() {
        console.log('Cat meows.');
    }
}

function makeAnimalSpeak(animal) {
    animal.speak();
}

const dog = new Dog();
const cat = new Cat();

makeAnimalSpeak(dog); // Output: Dog barks.
makeAnimalSpeak(cat); // Output: Cat meows.

In this example, the makeAnimalSpeak function accepts an object of type Animal and calls its speak method. This demonstrates polymorphism because both Dog and Cat are treated as Animal objects and their respective speak methods are called.

Object Literal

An object literal is simply an object defined with a pair of curly braces {}. Here's an example:

const user = {
    username: "harsh",
    loginCount: 8,
    signedIn: true,

    getUserDetails: function(){
        console.log("Got user details");
        console.log(`username: ${this.username}`); // outputs: harsh
        console.log(this); //it will print the current context
    }
}

console.log(user.username); // outputs: harsh
console.log(this); // outputs: {}
console.log(user.getUserDetails());

//output of "console.log(user.getUserDetails());"

//Got user details from database
//username: harsh
//{
//  username: 'harsh',
  //loginCount: 8,
  //signedIn: 'true',
  //getUserDetails: [Function: getUserDetails]
//}
//undefined

Constructor Function

A constructor function is used to create multiple instances from a single object or object literal. When we use the new keyword, an empty object is created, and the constructor function packs all the arguments inside this instance, injecting them with the this keyword.

Example of Constructor Function:

function User(username, loginCount, isLoggedIn){
    this.username = username;
    this.loginCount = loginCount;
    this.isLoggedIn = isLoggedIn;

    return this; // It's optional to write this; it will implicitly return the values
}

const userOne = User("harsh", 12, true);
console.log(userOne);
// outputs: all the other values inside this, and in the end:
// username: 'harsh'
// loginCount: 12
// isLoggedIn: true

const userTwo = User("daksh", 18, true);
console.log(userOne); // values are overridden by userTwo, and data is changed

To create a new instance or copy that doesn't affect the original or other copies, we use the new keyword:

const userOne = new User("harsh", 12, true);
const userTwo = new User("daksh", 18, false);
console.log(userOne);
console.log(userTwo);

//outputs: 
//User { username: 'harsh', loginCount: 8, isLoggedIn: true }
//User { username: 'ChaiAurCode', loginCount: 12, isLoggedIn: false }

When we use the new keyword, the following steps occur:

  1. An empty object is created, known as an instance.

  2. The constructor function is called, which packs all our arguments inside the instance.

  3. The this keyword injects all the arguments inside the instance.

  4. The instance is returned.

Printing the Constructor

console.log(userOne.constructor);
// outputs: [Function: User]

Here, the constructor property is a reference to the function that created the instance.

By understanding these concepts, we can effectively use OOP in JavaScript to write more modular, reusable, and maintainable code.