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
What are OOP Concepts in JavaScript?
Objects and Classes in JavaScript
The Four Pillars of OOP
Encapsulation
Inheritance
Polymorphism
Abstraction
Other OOP Concepts in JavaScript
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. TheborrowBook
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 thebalance
property, making it private using the#
syntax.The
deposit
andwithdraw
methods provide controlled access to modify thebalance
.The
getBalance
method allows read-only access to thebalance
.
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
andDesigner
classes inherit fromEmployee
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 calledwork()
.The
Developer
andDesigner
subclasses override thework()
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
andsendMessage
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
andprivateMethod
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 exportsadd
andsubtract
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.