Skip to main content

Object-Oriented Programming (OOP) in JavaScript

Object-oriented programming models code around objects (data + behaviour). Key concepts: encapsulation, abstraction, inheritance, polymorphism, and composition.

Object literals

A simple object

const person = {
name: "Alice",
age: 30,
sayHi() {
console.log(`Hi, I'm ${this.name}`);
}
};
person.sayHi(); // Hi, I'm Alice

old style object: Constructor functions + prototypes

function Animal(type) {
this.type = type;
}
Animal.prototype.speak = function() {
console.log(`${this.type} makes a sound`);
};
const dog = new Animal('Dog');
dog.speak();

Why prototypes? Methods on the prototype are shared by all instances (memory efficient).

ES6 classes

class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log(`Hi, I'm ${this.name}`);
}
}
const p = new Person('Jeff', 62);
p.sayHi();

Inheritance

The extends keyword is used in JavaScript classes to create a subclass (child class) from a parent class (base class). This allows the child class to inherit properties and methods from the parent class. It enables code reuse and supports inheritance, which is a core concept of OOP.

The super keyword is used to call the parent class’s constructor or methods. It is often used inside the child class’s constructor to initialize values from the parent. Without calling super(...), you cannot use this in a child class constructor. super.methodName() is used to call a method from the parent class. Once the object is created, you cannot access the parent methods via super from the outside.

class Employee extends Person {
constructor(name, age, role) {
super(name, age);
this.role = role;
}
sayHi() {
console.log(`Hi, I'm ${this.role} ${this.name}`);
}
describe() {
console.log(`${this.name}, ${this.role}`);
}
}
const e = new Employee('Jeff', 34, 'Engineer');
e.sayHi(); // Hi, I'm Engineer Jeff
// e.super.sayHi(); Syntax Error
e.describe(); // Jeff, Engineer
console.log(e.name); // Jeff

Encapsulation (private fields & getters/setters)

What is Encapsulation?

  • Encapsulation is one of the core principles of object-oriented programming.
  • It means hiding the internal state (data) of an object and only allowing it to be accessed or modified through well-defined methods.
  • Helps to protect data from unintended access and maintain control over object behavior.

Benefits of Encapsulation

  • Data protection: Prevents external code from directly changing internal state.
  • Control access: You decide which properties or methods are public or private.
  • Flexibility: Internal implementation can change without affecting external code.
  • Maintainability: Easier to debug and modify code since access is controlled.

Using private fields (ES2022+)

  • Prefix a property with # to make it truly private.
  • Can only be accessed within the class.
class BankAccount {
#balance = 0; // private field

constructor(owner) {
this.owner = owner;
}

deposit(amount) {
this.#balance += amount;
}

withdraw(amount) {
if (amount <= this.#balance) {
this.#balance -= amount;
return amount;
} else {
console.log("Insufficient funds");
return 0;
}
}

getBalance() {
return this.#balance;
}
}

const acc = new BankAccount("Alice");
acc.deposit(100);
console.log(acc.getBalance()); // 100
// console.log(acc.#balance); // Syntax Error: Private field '#balance' must be declared in an enclosing class

Using getters and setters

  • Provides controlled access to properties while keeping the internal representation hidden.
class Person {
#name;

constructor(name) {
this.#name = name;
}

get name() {
return this.#name;
}

set name(newName) {
if (newName.length > 0) {
this.#name = newName;
} else {
console.log("Name cannot be empty");
}
}
}

const p = new Person("Alice");
console.log(p.name); // Alice
p.name = "Bob";
console.log(p.name); // Bob
p.name = ""; // Name cannot be empty

Key Points

  • Private fields (#) are the modern way to enforce encapsulation.
  • Getters and setters allow controlled access to internal data.
  • Encapsulation improves security, maintainability, and flexibility.
  • External code should never directly manipulate internal state; always use methods.

Polymorphism

Polymorphism is an OOP principle that means "many forms" . It allows objects of different classes to be treated as objects of a common parent class through method overriding. With polymorphism, the same method name can behave differently depending on the object that calls it.

Benefits of Polymorphism

  • Code reusability: Same method can be used for different object types.
  • Flexibility: Functions or methods can work with objects of multiple types.
  • Maintainability: Adding new subclasses does not require changing existing code that uses the parent class.

Method Overriding

  • A child class provides its own implementation of a method from the parent class.
class Animal {
speak() {
console.log("Animal makes a sound");
}
}

class Dog extends Animal {
speak() {
console.log("Dog barks");
}
}

class Cat extends Animal {
speak() {
console.log("Cat meows");
}
}

new Dog().speak(); // Dog barks
new Cat().speak(); // Cat meows

Using super with overridden methods

  • The child can extend the behavior of the parent method instead of completely replacing it.
class Dog extends Animal {
speak() {
super.speak(); // call parent method
console.log("Dog barks loudly");
}
}


new Dog().speak();
// Animal makes a sound
// Dog barks loudly

Composition

What is Composition?

  • Composition is an OOP principle where objects are built from smaller, reusable components instead of inheriting from a parent class.
  • It emphasizes "has-a" relationships instead of "is-a" relationships.
  • The idea is to combine simple objects to create more complex behavior rather than relying on deep inheritance hierarchies.

It is a good practice to compose objects (holding instances) instead of deep inheritance chains.

  • Flexibility: You can mix and match behaviors without creating deep class chains.
  • Avoids tight coupling: Inheritance can create fragile code when parent classes change.
  • Code reuse: Share behaviors between objects without forcing an inheritance relationship.
  • Easier to maintain and test: Smaller components are easier to understand.

Using Composition

const canEat = {
eat() {
console.log(`${this.name} is eating`);
}
};

const canWalk = {
walk() {
console.log(`${this.name} is walking`);
}
};

function createPerson(name) {
const person = { name };
return Object.assign(person, canEat, canWalk);
}

const alice = createPerson("Alice");
alice.eat(); // Alice is eating
alice.walk(); // Alice is walking

canEat and canWalk are reusable behaviors. createPerson composes these behaviors into a single object. No inheritance is used; behaviors can be mixed into any object.

A Car has an Engine and GPS (composition) rather than Car extends Engine.

const Engine = {
start() {
console.log(`${this.model}'s engine started`);
}
};

const GPS = {
navigate(destination) {
console.log(`Navigating to ${destination}`);
}
};

function createCar(model) {
const car = { model };
return Object.assign(car, Engine, GPS);
}

const myCar = createCar("Tesla");
myCar.start(); // Tesla's engine started
myCar.navigate("Home"); // Navigating to Home

Object Class

In JavaScript, Object is the root class from which almost all objects inherit. Every object in JS is either created from Object directly or indirectly. It provides common methods and properties available to all objects.

Creating Objects

// Using object literal
const obj1 = { a: 1, b: 2 };

// Using Object constructor
const obj2 = new Object();
obj2.a = 1;
obj2.b = 2;

Object.prototype Methods

  • toString() – returns a string representation of the object.
  • hasOwnProperty(prop) – checks if a property exists directly on the object (not in prototype).
  • valueOf() – returns the primitive value of the object.
  • isPrototypeOf(obj) – checks if an object exists in another object’s prototype chain.
  • propertyIsEnumerable(prop) – checks if a property is enumerable.
  • Object.create(proto) - creates a new object with the specified prototype.
  • Object.assign() - copies enumerable properties from source objects to a target.
  • Object.keys() - Returns an array of the object’s own property names.
  • Object.values() - Returns an array of the object’s own property values.
  • Object.entries() - Returns an array of key-value pairs for each property.

Example:

const obj = { name: "Alice" };
console.log(obj.toString()); // [object Object]
console.log(obj.hasOwnProperty("name")); // true
console.log(obj.hasOwnProperty("age")); // false
console.log(obj.valueOf()); // { name: 'Alice' }

const proto = {
greet() {
console.log(`Hello, my name is ${this.name}`);
}
};
const person = Object.create(proto);
person.name = "Bob";
person.greet(); // Hello, my name is Bob

const obj2 = { a: 1, b: 2 };
console.log(Object.keys(obj2)); // ['a', 'b']
console.log(Object.values(obj2)); // [1, 2]
console.log(Object.entries(obj2));// [['a',1], ['b',2]]