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
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
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:
Static Properties and Methods
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:
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:
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
- 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:
- 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:
- 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:
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
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 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