OOP in JavaScript (Object-Oriented Programming)
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects" that contain data (properties) and code (methods). OOP helps structure code, making it more understandable, reusable, and easier to maintain.
JavaScript supports OOP but implements it somewhat differently than classical object-oriented languages (Java, C++). Before ES6 (2015), OOP in JavaScript was built on prototypes, and with ES6, class syntax appeared, which is syntactic sugar over prototypal inheritance.
Core OOP Principles
Encapsulation
Bundling data and methods that work with that data into a single entity (object or class). Hiding internal implementation and providing a public interface for interaction.
Inheritance
A mechanism that allows creating new classes based on existing ones, inheriting their properties and methods. This promotes code reuse and creation of class hierarchies.
Polymorphism
The ability of objects with the same interface to have different implementations. The same method can behave differently depending on the object that calls it.
Abstraction
Highlighting the main, most significant characteristics of an object and ignoring secondary ones. Simplifying complex systems by modeling classes corresponding to the problem domain.
Classes in JavaScript (ES6+)
With ES6, class syntax was added to JavaScript, making code more readable and familiar to developers acquainted with classical OOP.
Class Declaration
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old`);
}
celebrateBirthday() {
this.age++;
console.log(`Happy birthday! Now I'm ${this.age} years old`);
}
}
const person = new Person('Alex', 25);
person.greet();
person.celebrateBirthday();
Class Components:
constructor— special method for object initializationthis— reference to current class instance- Class methods — functions available to all instances
Encapsulation
Encapsulation allows hiding internal implementation details and providing only the necessary interface for interaction.
Private Fields (ES2022)
class BankAccount {
#balance = 0;
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
console.log(`Deposited ${amount}. Balance: ${this.#balance}`);
}
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
console.log(`Withdrawn ${amount}. Balance: ${this.#balance}`);
} else {
console.log('Insufficient funds');
}
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance());
Private fields (with # prefix) are inaccessible from outside the class. This provides true encapsulation.
Getters and Setters
class Rectangle {
constructor(width, height) {
this._width = width;
this._height = height;
}
get area() {
return this._width * this._height;
}
get perimeter() {
return 2 * (this._width + this._height);
}
set width(value) {
if (value > 0) {
this._width = value;
} else {
console.log('Width must be positive');
}
}
set height(value) {
if (value > 0) {
this._height = value;
} else {
console.log('Height must be positive');
}
}
}
const rect = new Rectangle(10, 5);
console.log(rect.area);
rect.width = 15;
console.log(rect.area);
Inheritance
Inheritance allows creating new classes based on existing ones, extending or modifying their functionality.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
move() {
console.log(`${this.name} moves`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
console.log(`${this.name} barks: Woof-woof!`);
}
fetch() {
console.log(`${this.name} fetches the ball`);
}
}
class Cat extends Animal {
constructor(name, color) {
super(name);
this.color = color;
}
speak() {
console.log(`${this.name} meows: Meow!`);
}
scratch() {
console.log(`${this.name} scratches`);
}
}
const dog = new Dog('Buddy', 'Labrador');
dog.speak();
dog.move();
dog.fetch();
const cat = new Cat('Whiskers', 'Orange');
cat.speak();
cat.move();
cat.scratch();
Key Points:
extends— keyword for inheritancesuper()— calls parent class constructor- Child class can override parent methods (polymorphism)
Polymorphism
Polymorphism allows using objects of different classes through a common interface.
class Shape {
constructor(name) {
this.name = name;
}
calculateArea() {
throw new Error('Method calculateArea must be implemented');
}
describe() {
console.log(`This is a ${this.name} with area ${this.calculateArea()}`);
}
}
class Circle extends Shape {
constructor(radius) {
super('Circle');
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius ** 2;
}
}
class Square extends Shape {
constructor(side) {
super('Square');
this.side = side;
}
calculateArea() {
return this.side ** 2;
}
}
class Triangle extends Shape {
constructor(base, height) {
super('Triangle');
this.base = base;
this.height = height;
}
calculateArea() {
return (this.base * this.height) / 2;
}
}
const shapes = [
new Circle(5),
new Square(4),
new Triangle(6, 3)
];
shapes.forEach(shape => {
shape.describe();
});
Each shape implements the calculateArea() method in its own way, but all can be used through a common interface.
Static Methods and Properties
Static methods and properties belong to the class, not its instances.
class MathHelper {
static PI = 3.14159;
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
static circleArea(radius) {
return this.PI * radius ** 2;
}
}
console.log(MathHelper.add(5, 3));
console.log(MathHelper.circleArea(10));
console.log(MathHelper.PI);
Factory Methods
class User {
constructor(name, email, role) {
this.name = name;
this.email = email;
this.role = role;
}
static createAdmin(name, email) {
return new User(name, email, 'admin');
}
static createGuest(name) {
return new User(name, 'guest@example.com', 'guest');
}
static createModerator(name, email) {
return new User(name, email, 'moderator');
}
getInfo() {
return `${this.name} (${this.role}) - ${this.email}`;
}
}
const admin = User.createAdmin('John', 'john@example.com');
const guest = User.createGuest('Guest');
const moderator = User.createModerator('Mary', 'mary@example.com');
console.log(admin.getInfo());
console.log(guest.getInfo());
console.log(moderator.getInfo());
Abstraction
Abstraction allows highlighting the main points and hiding implementation details.
class Database {
connect() {
throw new Error('Method connect must be implemented');
}
disconnect() {
throw new Error('Method disconnect must be implemented');
}
query(sql) {
throw new Error('Method query must be implemented');
}
}
class MySQLDatabase extends Database {
connect() {
console.log('Connecting to MySQL');
}
disconnect() {
console.log('Disconnecting from MySQL');
}
query(sql) {
console.log(`Executing MySQL query: ${sql}`);
return [];
}
}
class PostgreSQLDatabase extends Database {
connect() {
console.log('Connecting to PostgreSQL');
}
disconnect() {
console.log('Disconnecting from PostgreSQL');
}
query(sql) {
console.log(`Executing PostgreSQL query: ${sql}`);
return [];
}
}
function executeQuery(database, sql) {
database.connect();
const result = database.query(sql);
database.disconnect();
return result;
}
const mysql = new MySQLDatabase();
const postgres = new PostgreSQLDatabase();
executeQuery(mysql, 'SELECT * FROM users');
executeQuery(postgres, 'SELECT * FROM products');
Composition vs Inheritance
Composition is an alternative to inheritance where an object contains other objects instead of inheriting from them.
class Engine {
start() {
console.log('Engine started');
}
stop() {
console.log('Engine stopped');
}
}
class GPS {
getLocation() {
return { lat: 55.7558, lon: 37.6173 };
}
}
class Radio {
play(station) {
console.log(`Playing radio: ${station}`);
}
}
class Car {
constructor() {
this.engine = new Engine();
this.gps = new GPS();
this.radio = new Radio();
}
start() {
this.engine.start();
console.log('Car is ready to go');
}
navigate() {
const location = this.gps.getLocation();
console.log(`Current location: ${location.lat}, ${location.lon}`);
}
listenMusic(station) {
this.radio.play(station);
}
}
const car = new Car();
car.start();
car.navigate();
car.listenMusic('Rock FM');
Composition Advantages:
- More flexible structure
- Avoiding deep inheritance hierarchies
- Easier to test and modify
Prototypal OOP (Before ES6)
Before classes appeared, OOP in JavaScript was implemented through constructor functions and prototypes.
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};
Person.prototype.celebrateBirthday = function() {
this.age++;
console.log(`Now I'm ${this.age} years old`);
};
function Developer(name, age, language) {
Person.call(this, name, age);
this.language = language;
}
Developer.prototype = Object.create(Person.prototype);
Developer.prototype.constructor = Developer;
Developer.prototype.code = function() {
console.log(`${this.name} codes in ${this.language}`);
};
const dev = new Developer('Anna', 28, 'JavaScript');
dev.greet();
dev.code();
dev.celebrateBirthday();
Important:
Modern class syntax (ES6+) is syntactic sugar over prototypal inheritance. Under the hood, JavaScript still uses prototypes.
OOP Patterns in JavaScript
Singleton
class Singleton {
static instance = null;
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
this.data = [];
}
addData(item) {
this.data.push(item);
}
getData() {
return this.data;
}
}
const instance1 = new Singleton();
instance1.addData('First item');
const instance2 = new Singleton();
instance2.addData('Second item');
console.log(instance1 === instance2);
console.log(instance1.getData());
Factory
class Button {
constructor(text) {
this.text = text;
}
render() {
console.log(`Button: ${this.text}`);
}
}
class Input {
constructor(placeholder) {
this.placeholder = placeholder;
}
render() {
console.log(`Input field: ${this.placeholder}`);
}
}
class Select {
constructor(options) {
this.options = options;
}
render() {
console.log(`Dropdown: ${this.options.join(', ')}`);
}
}
class FormElementFactory {
static createElement(type, config) {
switch (type) {
case 'button':
return new Button(config.text);
case 'input':
return new Input(config.placeholder);
case 'select':
return new Select(config.options);
default:
throw new Error('Unknown element type');
}
}
}
const button = FormElementFactory.createElement('button', { text: 'Submit' });
const input = FormElementFactory.createElement('input', { placeholder: 'Enter name' });
const select = FormElementFactory.createElement('select', { options: ['Option 1', 'Option 2'] });
button.render();
input.render();
select.render();
OOP Advantages
-
Modularity
Code is divided into independent objects, each responsible for its functionality. -
Reusability
Classes and objects can be reused in different parts of the application. -
Extensibility
Easy to add new functionality through inheritance or composition. -
Maintainability
Changes in one class don't affect other parts of the system (with proper architecture). -
Abstraction
Hiding complexity and providing a simple interface for interaction.
OOP Disadvantages
-
Complexity
For small tasks, OOP can be excessive. -
Performance
Creating many objects can affect performance. -
Deep Hierarchies
Excessive use of inheritance leads to complex and fragile structures. -
Over-abstraction
Too many levels of abstraction complicate code understanding.
When to Use OOP
Suitable for:
- Large applications with many interconnected entities
- Projects requiring clear structure and hierarchy
- Systems requiring code reuse
- Team development with clear separation of responsibilities
Not suitable for:
- Simple scripts and utilities
- Functional programming
- Cases where function composition is more effective
Summary:
OOP in JavaScript is a powerful paradigm for structuring code. Modern class syntax makes code more readable, but it's important to remember that under the hood JavaScript uses prototypal inheritance. Choose OOP when you need clear structure, encapsulation, and code reuse. For more flexible solutions, consider composition or functional programming.