Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects. Objects can contain data (in the form of properties) and methods (functions associated with the object). OOP is widely used to design reusable and modular code, making it easier to manage complexity in large-scale applications.

ECMAScript 2015, also known as ES6 and ES2015, introduced JavaScript Classes. JavaScript classes are syntactic sugar over the prototypal inheritance. You can think of an ES6 class as a constructor function with much prettier syntax. Using ES6 Classes, we can easily implement OOP in Javascript.


Class and Object

  • Object is a thing which can be seen or touched and in software we try to represent the same real-life thing with object.
  • Object Oriented Programing is nothing but code around that object.
  • Class is not an object, it’s like blueprint which can generate objects. So, class helps to set the classification of objects with its properties and capabilities.
  • Any number of instance can be created from a class, each instance is called Object.
class Car
{
    /* Properties and Actions */
}

let lancer = new Car();

console.log(typeof Car); // function
console.log(typeof lancer); // object
console.log(lancer instanceof Car); // true

In the above code snippet, we can see the typeof class Car still return as function because behind the scene JavaScript still works with function only.


Constructor, Properties and Methods

  • Constructor is just a function which gets called automatically when we create an instance from the class.
  • Instance variables get created and initialized using constructor.
  • Instance variables are nothing but called properties of the object.
  • Methods are again functions which attached with instance and all instance created from the same class will have those methods or actions.
  • Accessing properties and methods inside class, we need this keyword.
class Meetup
{
    constructor(name, location)
    {
        this.name = name;
        this.location = location;
    }

    start()
    {
        console.log(`${this.name} meetup is started at ${this.location}`);
    }
}

let jsMeetup = new Meetup('JS', 'Baku');
let vueMeetup = new Meetup('VueJS', 'Khankendi');

jsMeetup.start(); // JS meetup is started at Baku
vueMeetup.start(); // VueJS meetup is started at Khankendi

Explanation of above code snippet:

  • The instance jsMeetup created using new keyword and class Meetup has been called like function.
  • We can pass arguments to the class as well like any other function and behind the scene, it will call to the constructor function for initializing the instance variables name and location.

Static Properties and Methods

  • Each instance of the class will have its own properties which gets created at constructor but Class can have also it’s own properties.
  • The class only properties are called Static Properties.
  • Same holds true for Static methods as well.
class Meetup
{
    constructor(name, location)
    {
        this.name = name;
        this.location = location;
    }

    start()
    {
        console.log(`${this.name} meetup is started at ${this.location}`);
    }

    static getAddress()
    {
        console.log('Returned Address');

        /* this.location will return undefined */
        console.log(`City: ${this.location}`);
    }
}

Meetup.admin = "Orkhan Alishov";
Meetup.getMembers = function () {
    console.log(`${Meetup.admin} Returned Members`);
}

let jsMeetup = new Meetup('JS', 'Baku');
console.log(Meetup.admin); // Orkhan Alishov
console.log(jsMeetup.admin); // undefined

Meetup.getMembers(); // Orkhan Alishov Returned Members
jsMeetup.getMembers(); // TypeError: jsMeetup.getMembers is not a function

Meetup.getAddress(); // Returned Address City: undefined
jsMeetup.getAddress(); // TypeError: jsMeetup.getAddress is not a function

Explanation of above code snippet:

  • The instance jsMeetup cannot access the class only methods i.e. static methods getMembers() and getAddress(). Same with static properties as well like admin is static property.
  • We can put label as static in-front of methods and properties inside the class definition to make it accessible to class only or we can add it later to the class as well like Meetup.getMembers = function(){...}.
  • The scope of this keyword is current execution context of the method. In case of static method, the execution context can never be class instance or object. That’s the reason this.location of static method getAddress() prints undefined.

Getter and Setter

class Meetup
{
    constructor(name)
    {
        this._name = name;
    }

    get name()
    {
        return this._name;
    }

    set name(val)
    {
        this._name = val;
    }
}

let meetup = new Meetup('JS');
console.log(`Meetup name: ${meetup.name}`); // Meetup name: JS

meetup.name = 'VueJS';
console.log(`Meetup name: ${meetup.name}`); // Meetup name: VueJS

Explanation of above code snippet:

  • With getter and setter, will have more control on object properties after initialization with constructor.
  • We can do required validation on data within get and set method before setting or getting the value.
  • We can see in the above code that the instance property name is _name but we are using it as meetup.name and its working fine because of getter and setter methods.

Inheritance in ES6

class Meetup
{
}

class TechMeet extends Meetup
{
}

class SportMeet extends Meetup
{
}

let js = new TechMeet();

console.log(js instanceof TechMeet); // true
console.log(js instanceof Meetup); // true
console.log(js instanceof Object); // true
  • The above code snippet is new way of achieving inheritance in JavaScript using extends and class keyword.
  • We can see that object js is instance of class TechMeet and Meetup both because class TechMeet extends parent class Meetup.

- Example of Inheritance with constructor

class Meetup
{
    constructor()
    {
        console.log("Inside Meetup constructor");
    }
}

class TechMeet extends Meetup
{
    constructor()
    {
        super();
        console.log("Inside TechMeet constructor");
    }
}

let js = new TechMeet();
// Inside Meetup constructor
// Inside TechMeet constructor

Explanation of above code snippet:

  • Inside constructor function of child class TechMeet, we have to call super() method to call the parent constructor first otherwise JavaScript will throw error.
  • super() method is nothing but constructor function of Parent class.
  • super() call is must in constructor of derived class whether explicit presence of parent constructor exists or not.

- One more example on extends and super()

class Meetup
{
    constructor(organizer)
    {
        this.organizer = organizer;
    }
}

class JsMeet extends Meetup
{
    constructor(organizer)
    {
        super(organizer);
    }
}

class CssMeet extends Meetup
{
    constructor(organizer)
    {
        super(organizer);
        this.organizer = 'CSS';
    }
}

let js = new JsMeet('Mr. JS');
console.log(js.organizer); // Mr. JS

let css = new CssMeet('Mr. CSS');
console.log(css.organizer); // CSS

Explanation of above code snippet:

  • We can pass the argument from child constructor to parent constructor through super() method like above code snippet.
  • We can override the parent class properties inside constructor of child class.

- More on Inheritance with super keyword

class Meetup
{
    organize()
    {
        console.log('Organizing meetup');
    }

    static getMeetupFounderDetails()
    {
        console.log("Meetup founder details");
    }
}

class TechMeet extends Meetup
{
    organize()
    {
        // super.organize();
        console.log('Organizing TechMeet');
        super.organize();
    }

    static getMeetupFounderDetails()
    {
        console.log("TechMeet founder details");
        super.getMeetupFounderDetails();
    }
}

let js = new TechMeet();
js.organize(); // Organizing TechMeet | Organizing meetup

TechMeet.getMeetupFounderDetails(); // TechMeet founder details | Meetup founder details

Explanation of above code snippet:

  • Child class can access the methods of parent class using super object like super.organise().
  • Similarly static methods of child class can access the static method of parent class with help of super object.

this keyword

this keyword is used to refer to the instance of the class. If we define a variable in a method using this e.g. this.num, we can access it through an instance like this - person1.num. If we want to use a variable inside multiple methods, we should define it using this.

An example:

class Person
{
    printMsg1()
    {
        this.num = 10;
        console.log(`PrintMsg1: num = ${this.num}`);
    }

    printMsg2()
    {
        this.num = 20;
        console.log(`PrintMsg2: num = ${this.num}`);
    }

    printMsg3()
    {
        this.num = 30;
        console.log(`PrintMsg3: num = ${this.num}`);
    }
}

let person1 = new Person();
person1.printMsg1(); // PrintMsg1: num = 10
person1.printMsg2(); // PrintMsg2: num = 20
person1.printMsg3(); // PrintMsg3: num = 30

console.log(`num = ${person1.num}`); // num = 30

static keyword

By definition, static properties and methods are bound to a class, not the instances of that class. Therefore, static methods are useful for defining helper or utility methods, such as functions to create or clone objects, whereas static properties are useful for caches, fixed-configuration, or any other data you don’t need to be replicated across instances. Static methods have no access to data stored in specific instances.

We define static properties and methods using the static keyword. Static properties and methods are called directly on the class itself. If we attempt to call a static property or method from an instance, we’ll get an error. To use a static property or method in a class method or outside the class, you use the class name followed by the . and the static method.

class Person
{
    static value = 15;

    static greeting()
    {
        console.log(`Value is ${Person.value}`);
    }
}

console.log(Person.value); // 15
Person.greeting(); // Value is 15

Private

The ECMAScript 2022 update shipped with support for private values inside JavaScript classes. Private properties and methods are accessible only inside the class. It is a syntax error to refer to private fields from outside the class. You can declare private properties inside a class by prefixing the variable with the # sign. Public and private fields do not conflict, so we can have both public and private fields with same name in a class with private field prefixed with # sign. In the below example, we can have both private #printMsg and public printMsg methods in the same class.

class Person
{
    #printMsg()
    {
        console.log("A private message");
    }

    printMsg()
    {
        console.log("A public message");
        this.#printMsg();
    }
}

let person1 = new Person();
person1.printMsg(); // A public message | A private message

4 Pillars of Object-Oriented Programming

  • Abstraction
  • Encapsulation
  • Inheritance
  • Polymorphism

Abstraction

Abstraction is a concept in which we hide the implementation detail of a method and only expose the important things or attributes to the user, like in the code below.

We have two methods learnSkill and isEligibleForVote,

  • The first method is just updating the skills array.
  • The second method is checking either the user is eligible for vote.

The user shouldn't concern about the methods implementation detail and just use them in their code.

class User
{
    constructor(name, email, age)
    {
        this._name = name;
        this._email = email;
        this._age = age;
        this._skills = [];
    }

    learnSkill(skill)
    {
        this._skills.push(skill);
    }

    isEligibleForVote()
    {
        return this._age >= 18;
    }
}

const user = new User('Orkhan Alishov', 'hi@alishoff.com', 25);

user.learnSkill('VueJS');
user.learnSkill('JS');

console.log(user._skills); // (2) ['VueJS', 'JS']
console.log(user.isEligibleForVote()); // true

Abstraction hides implementation details and exposes only the essential features of an object.

In JavaScript, you can achieve abstraction using abstract classes (emulated via base classes) or interfaces (emulated using TypeScript).

class Vehicle
{
    constructor(type)
    {
        if (this.constructor === Vehicle) {
            throw new Error('Abstract classes cannot be instantiated.');
        }

        this.type = type;
    }

    move()
    {
        throw new Error('Method "move()" must be implemented.');
    }
}

class Car extends Vehicle
{
    move()
    {
        console.log(`The ${this.type} car drives on roads.`);
    }
}

class Boat extends Vehicle
{
    move()
    {
        console.log(`The ${this.type} boat sails on water.`);
    }
}

// const vehicle = new Vehicle('generic'); // Uncaught Error: Abstract classes cannot be instantiated.
const car = new Car('sports');
const boat = new Boat('fishing');

car.move(); // The sports car drives on roads.
boat.move(); // The fishing boat sails on water.

Encapsulation

Encapsulation is a concept the variables or data in classes cannot be access directly from an object and should be private, there should be getter and setter methods to do this like below. Here we have getter and setter method for name variable.

class User
{
    constructor(name, email, age)
    {
        this._name = name;
        this._email = email;
        this._age = age;
    }

    get name()
    {
        return this._name;
    }

    set name(newName)
    {
        this._name = newName;
    }

    getAge()
    {
        return `User age is ${this._age}`;
    }
}

const user = new User('Orkhan Alishov', 'hi@alishoff.com', 25);
console.log(user.name); // Orkhan Alishov

user.name = 'Naz Alishova';
console.log(user.name); // Naz Alishova

console.log(user.getAge()); // User age is 25

Encapsulation refers to bundling data (properties) and methods into a single unit (class). It also involves restricting direct access to some of the object's components using private or protected members.

class BankAccount
{
    #balance;

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

    deposit(amount)
    {
        this.#balance += amount;
        console.log(`Deposited ${amount}. New balance: ${this.#balance}`);
    }

    withdraw(amount)
    {
        if (amount > this.#balance) {
            console.log('Insufficient balance.');
        } else {
            this.#balance -= amount;
            console.log(`Withdrew ${amount}. Remaining balance: ${this.#balance}`);
        }
    }

    // Getter for balance (read-only access)
    getBalance()
    {
        return this.#balance;
    }
}

const account = new BankAccount('Alice', 1000);

account.deposit(500); // Deposited 500. New balance: 1500
account.withdraw(2000); // Insufficient balance.
console.log(account.getBalance()); // 1500

account.withdraw(500); // Withdrew 500. Remaining balance: 1000
console.log(account.getBalance()); // 1000

Inheritance

Inheritance is a concept in which we can extend our parent class to child class for example, if there is a class named Animal and second class Duck so the class Duck can be inherited from Animal class, like below we have Developer class which is inherited from the User class, and now the Developer class can use its parent class User it's variable and methods.

class User
{
    constructor(name, email, age)
    {
        this.name = name;
        this.email = email;
        this.age = age;
    }

    getAge()
    {
        return `User age is ${this.age}`;
    }
}

class Developer extends User
{
    constructor(name, email, age, skills)
    {
        super(name, email, age);
        this.skills = skills;
    }
}

const developer = new Developer('Orkhan Alishov', 'hi@alishoff.com', 25, ['html', 'css', 'js']);

console.log(developer.getAge()); // User age is 25
console.log(developer.skills); // (3) ['html', 'css', 'js']

Polymorphism

Polymorphism is also known as Method Overriding in some languages, this means that if the class is inherited from its parent class and both have the same methods, so the child class methods can be overridden or rewritten according to its functionality, like below we have same methods makeSound in different but inherited classes but their implementation detail is different according to their functionality they should perform.

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

class Duck extends Animal
{
    makesSound()
    {
        console.log('Quack quack');
    }
}

class Cat extends Animal
{
    makesSound()
    {
        console.log('Meow meow');
    }
}

const cat = new Cat();
cat.makesSound() // Meow meow

const duck = new Duck();
duck.makesSound() // Quack quack

Polymorphism allows methods in derived classes to override methods in the base class. It ensures the same method name behaves differently based on the context.

class Shape
{
    area()
    {
        console.log('Area is undefined for a generic shape.');
    }
}

class Rectangle extends Shape
{
    constructor(width, height)
    {
        super();
        this.width = width;
        this.height = height;
    }

    area()
    {
        return this.width * this.height;
    }
}

class Circle extends Shape
{
    constructor(radius)
    {
        super();
        this.radius = radius;
    }

    area()
    {
        return Math.PI * this.radius * this.radius;
    }
}

const shapes = [new Rectangle(10, 5), new Circle(7)];

shapes.forEach(shape => {
    console.log(`Area: ${shape.area()}`);
});

// Area: 50
// Area: 153.93804002589985

Key advantages of OOP in JavaScript

  • Reusability: Reuse code through inheritance.
  • Modularity: Organize code into classes and objects.
  • Scalability: Easier to extend and maintain.
  • Readability: Clear structure for complex programs.