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.
// 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).
// 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".
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.
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.
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.
// 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.
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).
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.
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.
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.
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.
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
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.
// 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
// 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.
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.
| Need | Good fit | Why |
|---|---|---|
| One or two related values | Object literal | Less ceremony and easy to read. |
| Many similar instances with shared methods | Class | Constructor and prototype methods provide a reusable blueprint. |
| Shared behavior across different types | Small base class or composition | Reuse only the behavior that truly belongs together. |
| One-off helper with no instance state | Function or module | A 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.
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:
- What is Object-Oriented Programming (OOP)?
- What are the four pillars of OOP? Explain each one briefly.
- What is the difference between a class and an object?
- What does the constructor method do, and when is it called?
- What is the difference between a property and a method?
- What is inheritance in JavaScript? How do you implement it?
- What are the extends and super keywords used for?
- What is a static method? How is it different from a regular method?
- What are getters and setters? When would you use them?
- What is encapsulation and why is it useful?
- Can a class extend more than one class in JavaScript?
- What is method overriding? Give an example.
- What is the difference between class syntax and constructor functions in JavaScript?
- What does this refer to inside a class method?
- How would you create a private property in a JavaScript class?
- What is the difference between _privateByConvention and #realPrivateField?
- Can static methods access instance properties directly? Why or why not?
- When would you use a getter/setter instead of a normal method call?