OOP & Classes in JavaScript

Why should you care about OOP & Classes in JavaScript?

OOP helps you model real things like users, products, and orders in clean reusable code. As your app grows, classes make your logic easier to organize, test, and maintain.

OOP stands for Object-Oriented Programming. It is a programming style that groups related data and behaviour into objects. An object is a value with properties and methods; a class is a definition you can use to create objects with a consistent shape and behaviour.

OOP can make code easier to organise when an application has many related entities, shared behaviour, or state that must be kept together. JavaScript is multi-paradigm, though: functions, modules, and object literals are also useful styles. Choose OOP when objects and shared behaviour make the design clearer, not because it is always better.

javascript

// Without OOP - scattered variables and functions
let car1Colour = "red";
let car1Speed = 0;

function accelerateCar1() {
  car1Speed += 10;
}

// With OOP - everything grouped neatly in a class
class Car {
  constructor(colour) {
    this.colour = colour;
    this.speed = 0;
  }

  accelerate() {
    this.speed += 10;
  }
}

const myCar = new Car("red");
myCar.accelerate();
console.log(myCar.speed); // 10

OOP is built on four core pillars: Encapsulation (bundling data and methods together), Abstraction (hiding complex details), Inheritance (one class borrowing from another), and Polymorphism (objects behaving differently depending on context). We will explore these ideas as we go through the sections below.

A class defines the properties and methods that its instances can have. The class itself is not an instance; calling it with new creates an object, also called an instance.

In JavaScript, you define a class using the class keyword, followed by the name of the class (by convention, class names start with a capital letter).

javascript

// Define a class
class Employee {
  // We will add details inside here
}

// Create objects (instances) from the class
const emp1 = new Employee();
const emp2 = new Employee();

console.log(emp1); // Employee {}
console.log(emp2); // Employee {}

// Each object is a separate instance
console.log(emp1 === emp2); // false

Right now the class is empty - it does not hold any data yet. In the next sections we will add a constructor, properties, and methods to it. The new keyword creates an instance from the class.

Classes were introduced in ES6 (2015). JavaScript classes provide a convenient syntax for defining objects and inheritance while JavaScript still uses prototypes internally. Constructor functions and prototype methods remain valid alternatives, especially in older code.

The constructor is a special method inside a class. It runs automatically the moment you create a new object using the new keyword. You use it to set up the initial data for the object.

Inside the constructor, this refers to the specific object being created. So this.name means "store the name on this object".

javascript

class Employee {
  constructor(name, department) {
    // Runs automatically when a new Employee is created
    this.name = name;
    this.department = department;
  }
}

// The values we pass in go straight into the constructor
const lead = new Employee("Sara Chen", "Engineering");
const dev  = new Employee("Mike Ross", "Design");

console.log(lead.name);       // Sara Chen
console.log(lead.department); // Engineering
console.log(dev.name);        // Mike Ross
console.log(dev.department);  // Design

Every class can only have one constructor. If you do not write one, JavaScript quietly provides an empty constructor behind the scenes. But as soon as your objects need any data on creation, you will want to write your own.

javascript

class Product {
  constructor(name, price = 0, inStock = true) {
    this.name = name;
    this.price = price;
    this.inStock = inStock;
  }
}

const defaultProduct = new Product("Notebook");
const premiumProduct = new Product("Mechanical Keyboard", 5999, false);

console.log(defaultProduct); // Product { name: "Notebook", price: 0, inStock: true }
console.log(premiumProduct); // Product { name: "Mechanical Keyboard", price: 5999, inStock: false }

Every object has two kinds of things: properties and methods.

  • Properties are the data stored on an object - like its name, age, or colour. They describe what the object is.
  • Methods are functions attached to an object - like drive, bark, or greet. They describe what the object can do.

javascript

class Dog {
  constructor(name, breed, colour) {
    // Properties - data about this dog
    this.name = name;
    this.breed = breed;
    this.colour = colour;
  }

  // Method - something the dog can do
  bark() {
    console.log(`${this.name} says: Woof!`);
  }

  // Method - another action
  fetch(item) {
    console.log(`${this.name} fetches the ${item}!`);
  }

  // Method - introduce the dog
  describe() {
    console.log(`${this.name} is a ${this.colour} ${this.breed}.`);
  }
}

const rex = new Dog("Rex", "Labrador", "golden");

// Accessing properties
console.log(rex.name);   // Rex
console.log(rex.breed);  // Labrador

// Calling methods
rex.bark();              // Rex says: Woof!
rex.fetch("tennis ball"); // Rex fetches the tennis ball!
rex.describe();          // Rex is a golden Labrador.

Methods use this.name to access the object's own data. In rex.bark(), the object before the dot is rex, so this inside bark refers to rex. The value of this depends on how the method is called, so storing a method in a separate variable can change or remove that object context.

You can add as many properties and methods as you need. That is the beauty of classes - they keep everything that belongs to a "thing" neatly packaged together.

Inheritance lets one class borrow features - properties and methods - from another class. The class that shares its features is called the parent class (or superclass). The class that borrows them is the child class (or subclass).

This is incredibly useful when you have shared behaviour. Instead of writing the same code twice, you write it once in the parent class and let the child class inherit it automatically.

In JavaScript, extends creates a subclass relationship. If the child defines its own constructor, it must call super() before using this; this calls the parent constructor and initializes the same derived instance. If the child has no constructor, JavaScript provides one that forwards the arguments to the parent.

javascript

// Parent class
class Animal {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  speak() {
    console.log(`${this.name} says: ${this.sound}!`);
  }

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

// Child class - inherits from Animal
class Dog extends Animal {
  constructor(name) {
    // super() calls the Animal constructor
    super(name, "Woof");
    this.tricks = []; // Dog-specific property
  }

  // Dog-specific method - not in Animal
  learnTrick(trick) {
    this.tricks.push(trick);
    console.log(`${this.name} learned: ${trick}`);
  }
}

// Another child class
class Cat extends Animal {
  constructor(name) {
    super(name, "Meow");
  }

  // Cat overrides the speak method
  speak() {
    console.log(`${this.name} purrs and says: ${this.sound}...`);
  }
}

const rex = new Dog("Rex");
rex.speak();             // Rex says: Woof!    (inherited from Animal)
rex.eat();              // Rex is eating.      (inherited from Animal)
rex.learnTrick("sit");  // Rex learned: sit   (Dog-only method)

const whiskers = new Cat("Whiskers");
whiskers.speak();       // Whiskers purrs and says: Meow... (overridden)

Notice that Cat overrides the speak method. When a child class defines a method with the same name as the parent, it replaces the parent version for that specific child. This is called method overriding.

javascript

class Vehicle {
  start() {
    console.log("Vehicle started");
  }
}

class ElectricCar extends Vehicle {
  start() {
    super.start();
    console.log("Battery system is now active");
  }
}

const tesla = new ElectricCar();
tesla.start();
// Vehicle started
// Battery system is now active

Inheritance helps you avoid repeating yourself (the DRY principle - Don't Repeat Yourself). Define shared behaviour once in the parent, and every child class gets it for free.

Static methods belong to the class itself, not to any individual object created from that class. You cannot call a static method on an instance - you call it directly on the class.

You define a static method with the static keyword before the method name. Static methods are related to the class but do not run as methods of an individual instance. For example, MathHelper.add(5, 3) is called on the class, while an instance method would be called on an object such as helper.add(5, 3).

javascript

class MathHelper {
  // Static method - belongs to the class, not an instance
  static add(a, b) {
    return a + b;
  }

  static multiply(a, b) {
    return a * b;
  }

  static square(n) {
    return n * n;
  }
}

// Call directly on the class - NO 'new' needed
console.log(MathHelper.add(5, 3));       // 8
console.log(MathHelper.multiply(4, 6));  // 24
console.log(MathHelper.square(7));       // 49

// This would throw an error:
// const helper = new MathHelper();
// helper.add(1, 2); // TypeError: helper.add is not a function

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  // Static method: useful for validation before creating an instance
  static isValidEmail(email) {
    return email.includes("@") && email.includes(".");
  }
}

console.log(User.isValidEmail("dev@example.com")); // true
console.log(User.isValidEmail("not-an-email"));      // false

You will see static methods used a lot in utility classes and helper libraries. JavaScript's built-in Math object is a great real example - Math.round(), Math.max(), and Math.floor() are methods you call on the object directly, not on an instance created with new.

javascript

class Employee {
  constructor(name, role) {
    this.name = name;
    this.role = role;
  }

  static fromObject(data) {
    return new Employee(data.name, data.role);
  }
}

const rawEmployee = { name: "Nina", role: "Frontend Developer" };
const emp = Employee.fromObject(rawEmployee);

console.log(emp.name); // Nina
console.log(emp.role); // Frontend Developer

Getters and setters give you control over how you read and write an object's properties. Instead of directly accessing a property, you can add logic in between - like validation, formatting, or calculation.

  • A getter lets you retrieve a value - it looks like a property but runs like a function.
  • A setter lets you control how a value is set - you can validate or transform the incoming value before storing it.

A getter controls how a value is read. A setter controls how a value is assigned, so it can validate or transform the incoming value before storing it.

In JavaScript, you define them with the get and set keywords. They are accessed like properties, not called like normal methods: use account.balance to read and account.balance = 500 to assign, without parentheses.

javascript

class BankAccount {
  constructor(owner, balance) {
    this.owner = owner;
    this._balance = balance; // Underscore signals "private by convention"
  }

  // Getter - read the balance
  get balance() {
    return `$${this._balance.toFixed(2)}`;
  }

  // Setter - control how balance is changed
  set balance(amount) {
    if (amount < 0) {
      console.log("Error: Balance cannot go below zero.");
      return;
    }
    this._balance = amount;
  }
}

const account = new BankAccount("Checking", 1000);

// Using the getter - looks like a property, no ()
console.log(account.balance); // $1000.00

// Using the setter - looks like a normal assignment
account.balance = 500;
console.log(account.balance); // $500.00

// Setter validation kicks in
account.balance = -100;       // Error: Balance cannot go below zero.
console.log(account.balance); // $500.00 (unchanged)

The underscore prefix on _balance is a convention (not enforced by JavaScript) to signal that it is an internal property that should not be accessed directly from outside the class. The getter and setter act as a controlled interface to it.

Getters and setters are a key part of encapsulation - one of the four pillars of OOP. They let you keep the internal workings of an object hidden while still allowing safe access from outside.

javascript

class UserProfile {
  constructor(firstName, lastName, age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this._age = age;
  }

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  set age(value) {
    if (value < 0 || value > 120) {
      console.log("Please enter a valid age.");
      return;
    }
    this._age = value;
  }

  get age() {
    return this._age;
  }
}

const profile = new UserProfile("Aarav", "Sharma", 24);
console.log(profile.fullName); // Aarav Sharma
profile.age = 25;
console.log(profile.age); // 25
profile.age = 200; // Please enter a valid age.

JavaScript now supports real private fields using the # symbol. A field like #balance can only be accessed from inside the same class.

This is stronger than the old underscore style like _balance. Underscore is only a naming convention, but #balance is actually enforced by JavaScript.

javascript

class Wallet {
  #balance = 0;

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

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

const wallet = new Wallet();
wallet.deposit(500);
console.log(wallet.getBalance()); // 500

// wallet.#balance = 999; // SyntaxError: private field cannot be accessed outside class

javascript

class Counter {
  #value = 0;

  increment() {
    this.#value += 1;
  }

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

const count = new Counter();
count.increment();
count.increment();
console.log(count.value); // 2

Both approaches can create objects whose shared methods are found through prototypes. Class syntax provides a structured way to define constructors, methods, and inheritance; constructor functions with explicit prototype methods are an older but still valid pattern.

They are not interchangeable in every detail, so you may encounter different behaviour around syntax, errors, and inheritance. Understanding both helps with interviews and existing codebases.

javascript

// Constructor function style (older)
function Person(name) {
  this.name = name;
}

Person.prototype.sayHi = function () {
  return `Hi, I am ${this.name}`;
};

const p1 = new Person("Ira");
console.log(p1.sayHi()); // Hi, I am Ira

javascript

// Class style (modern)
class PersonClass {
  constructor(name) {
    this.name = name;
  }

  sayHi() {
    return `Hi, I am ${this.name}`;
  }
}

const p2 = new PersonClass("Ira");
console.log(p2.sayHi()); // Hi, I am Ira

// Both share methods on prototype
console.log(typeof Person.prototype.sayHi); // function
console.log(typeof PersonClass.prototype.sayHi); // function

super() has two common jobs. In a child constructor, super(arguments) calls the parent constructor so the parent can initialize its properties. In a child method, super.method() calls the parent version so the child can extend it instead of duplicating it.

javascript

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

  describe() {
    return `${this.owner}'s account`;
  }
}

class SavingsAccount extends Account {
  constructor(owner, interestRate) {
    super(owner); // initialize Account before using this
    this.interestRate = interestRate;
  }

  describe() {
    return `${super.describe()} earns ${this.interestRate}% interest`;
  }
}

console.log(new SavingsAccount("Sam", 4).describe());

A derived constructor cannot use this before calling super(). The parent constructor must establish the instance first. Calling super() does not create a separate object; it initializes the same child instance through the parent logic.

Choose a class design or a simpler alternative.
NeedGood fitWhy
One or two related valuesObject literalLess ceremony and easy to read.
Many similar instances with shared methodsClassConstructor and prototype methods provide a reusable blueprint.
Shared behavior across different typesSmall base class or compositionReuse only the behavior that truly belongs together.
One-off helper with no instance stateFunction or moduleA class would add unnecessary structure.

Prefer composition when inheritance becomes deep: instead of building a long chain of classes, give an object small capabilities such as canLog or canSave. Inheritance is useful for a clear “is-a” relationship; composition is often easier to change when behavior varies independently.

Design check: before creating a class, ask whether you need many consistent instances, shared behavior, or protected construction rules. If not, an object literal, function, or module may be clearer.

Here is a quick recap of everything covered in this tutorial:

  • OOP is a programming style that organises code around objects (things) rather than just actions. It is built on four pillars: Encapsulation, Abstraction, Inheritance, and Polymorphism.
  • A class defines the structure and behaviour that its objects (instances) can have. Class names start with a capital letter by convention.
  • The constructor is a special method that runs automatically when a new object is created. It sets up the initial data using this.
  • Properties are data stored on an object (like this.name). Methods are functions attached to an object (like bark()).
  • Inheritance uses the extends keyword to let one class borrow features from another. Use super() in the child constructor to call the parent constructor.
  • Static methods are defined with the static keyword and belong to the class itself, not to any specific instance. Call them directly on the class.
  • Getters (get) let you read a value with logic. Setters (set) let you control how a value is written. They look like properties from the outside.
  • Private fields use the # notation (like #balance). They are truly private and cannot be accessed directly from outside the class.
  • Class syntax and constructor functions can both create objects with shared prototype methods, but they are different language features with different syntax and rules.
  • Classes were introduced in ES6 (2015) and provide convenient syntax for defining objects and inheritance in JavaScript's prototype-based language.

Use OOP when grouping state and shared behaviour makes a design clearer. For simpler cases, an object literal, function, or module may be the better choice.

These are common interview questions on OOP and Classes in JavaScript. Try to answer each one out loud in your own words before looking them up: