Object-Oriented Programming (OOP) is a programming paradigm based on four core principles:
Encapsulation is the bundling of data and the methods for processing it within a single object, as well as protecting that data from external interference. Access to the data is controlled through public methods (getters and setters).
In JavaScript, encapsulation can be implemented using classes and private fields (using #
).
Example:
class User {
#email; // Private field
constructor(name, email) {
this.name = name;
this.#email = email;
}
// Public method to get the email (getter)
getEmail() {
return this.#email;
}
// Public method to change the email (setter)
setEmail(newEmail) {
if (newEmail.includes('@')) {
this.#email = newEmail;
} else {
console.error('Error: invalid email');
}
}
getInfo() {
// Internal methods can work with private fields
return `User: ${this.name}, Email: ${this.#email}`;
}
}
const user = new User('John', 'john@test.com');
console.log(user.name); // 'John'
// console.log(user.#email); // Error! No access to the private field
console.log(user.getEmail()); // 'john@test.com'
user.setEmail('new-email@test.com');
console.log(user.getEmail()); // 'new-email@test.com'
user.setEmail('invalid-email'); // Error: invalid email
Inheritance allows a new class to be created based on an existing one, inheriting its properties and methods. This promotes code reuse and the creation of a class hierarchy.
Example:
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log('Animal makes a sound');
}
}
// The Dog class inherits from Animal
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call the parent class constructor
this.breed = breed;
}
// Overriding the parent method
makeSound() {
console.log('Dog barks: Woof-woof!');
}
// Dog's own method
wagTail() {
console.log(`${this.name} wags its tail.`);
}
}
const myDog = new Dog('Rex', 'Shepherd');
myDog.makeSound(); // 'Dog barks: Woof-woof!'
myDog.wagTail(); // 'Rex wags its tail.'
console.log(myDog.name); // 'Rex' (inherited from Animal)
Polymorphism (from Greek for “many forms”) is the ability of a function or method to process data of different types. In the context of OOP, this means that objects of different classes can react differently to the same method call.
In the example above, polymorphism is demonstrated by the makeSound()
method existing in both the Animal
and Dog
classes, but working differently.
Example:
class Cat extends Animal {
makeSound() {
console.log('Cat meows: Meow!');
}
}
const animals = [
new Animal('Creature'),
new Dog('Buddy', 'Mutt'),
new Cat('Whiskers', 'Siamese')
];
// A single interface to work with different objects
function printSound(animalList) {
for (const animal of animalList) {
animal.makeSound();
}
}
printSound(animals);
// Output:
// Animal makes a sound
// Dog barks: Woof-woof!
// Cat meows: Meow!
Here, we call the same makeSound()
method on different objects, and each one executes its own implementation.
Abstraction is the hiding of complex logic and providing a simple interface for interaction. The user doesn’t need to know how a method works, only what it does.
Classes themselves are a form of abstraction. When we create an object, we work with its methods without thinking about their internal implementation.
Example:
Imagine a complex object, like a CoffeeMachine
.
class CoffeeMachine {
#waterLevel; // Hidden details
constructor() {
this.#waterLevel = 0;
}
#heatWater() {
console.log('Heating water...');
}
#grindCoffee() {
console.log('Grinding beans...');
}
// Public method - a simple abstraction
makeEspresso() {
this.#grindCoffee();
this.#heatWater();
console.log('Your espresso is ready!');
}
}
const machine = new CoffeeMachine();
machine.makeEspresso(); // We just call one method
// We don't need to know about #heatWater() or #grindCoffee()
// All the complexity is hidden behind a simple interface.
The user simply calls makeEspresso()
, and the coffee machine does everything itself. This is abstraction — providing a simple interface to perform a complex task.