LearnFrenzy
Sign in Take a free test

this, Prototypes and Classes · Lesson 23 of 40

JavaScript Classes and Inheritance

5 min read Updated Nov 7, 2026 Saurabh Samir

The class syntax (ES2015, extended in ES2022) is the modern way to build objects that share behaviour. Under the hood it still uses prototypes, but it gives you a clean, familiar structure: a constructor, methods, getters and setters, static members, truly private fields, and inheritance with extends. Frameworks like Angular and Lightning Web Components are built around classes.

In this lesson

  • Constructors, fields and methods
  • Private #fields, getters and setters
  • Static members
  • extends, super, overriding and composition

A first class

class BankAccount {
  #balance = 0;                       // private field – invisible outside the class
  static count = 0;                   // static: belongs to the class itself

  constructor(owner, opening = 0) {
    this.owner = owner;               // public field
    this.#balance = opening;
    BankAccount.count++;
  }

  deposit(amount) {
    if (amount <= 0) throw new RangeError("Deposit must be positive");
    this.#balance += amount;
    return this;                      // returning this allows chaining
  }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error("Insufficient funds");
    this.#balance -= amount;
    return this;
  }

  get balance() { return this.#balance; }      // read-only property

  toString() { return `${this.owner}: ₹${this.#balance}`; }
}

const acc = new BankAccount("Asha", 1000);
acc.deposit(500).withdraw(200);
console.log(acc.balance, `${acc}`);
console.log(acc.owner, BankAccount.count);
console.log(Object.keys(acc));        // #balance isn't a normal property
try { acc.withdraw(5000); } catch (e) { console.log(e.message); }

Output

1300 Asha: ₹1300
Asha 1
[ 'owner' ]
Insufficient funds

Accessing acc.#balance from outside the class is a SyntaxError – it can't even be written. That's real encapsulation, unlike the old "_balance please don't touch" convention.

Getters and setters with validation

class Student {
  #marks = 0;
  constructor(name) { this.name = name; }

  get marks() { return this.#marks; }
  set marks(value) {
    if (!Number.isInteger(value) || value < 0 || value > 100) {
      throw new RangeError(`Invalid marks: ${value}`);
    }
    this.#marks = value;
  }
  get grade() { return this.#marks >= 75 ? "A" : this.#marks >= 40 ? "B" : "F"; }
}
const s = new Student("Ravi");
s.marks = 82;                        // calls the setter
console.log(s.marks, s.grade);
try { s.marks = 140; } catch (e) { console.log(e.message); }

Output

82 A
Invalid marks: 140

Static members

Static methods and fields belong to the class, not to instances – ideal for factory functions and utilities:

class Temperature {
  constructor(celsius) { this.celsius = celsius; }
  static fromFahrenheit(f) { return new Temperature(((f - 32) * 5) / 9); }
  static compare(a, b) { return a.celsius - b.celsius; }
  get fahrenheit() { return (this.celsius * 9) / 5 + 32; }
}
const t = Temperature.fromFahrenheit(98.6);
console.log(t.celsius.toFixed(1), new Temperature(100).fahrenheit);
const sorted = [new Temperature(30), new Temperature(-5)].sort(Temperature.compare);
console.log(sorted.map((x) => x.celsius));

Output

37.0 212
[ -5, 30 ]

Inheritance: extends and super

class Employee {
  constructor(name, salary) {
    this.name = name;
    this.salary = salary;
  }
  annualPay() { return this.salary * 12; }
  describe() { return `${this.name} earns ₹${this.annualPay().toLocaleString("en-IN")}/yr`; }
}

class Manager extends Employee {
  constructor(name, salary, reports) {
    super(name, salary);                 // must call super before using this
    this.reports = reports;
  }
  annualPay() {                          // override
    return super.annualPay() + this.reports.length * 50000;   // bonus per report
  }
  describe() {
    return `${super.describe()} and manages ${this.reports.length}`;
  }
}

const dev = new Employee("Asha", 90000);
const lead = new Manager("Kiran", 150000, ["Asha", "Ravi"]);
for (const person of [dev, lead]) console.log(person.describe());
console.log(lead instanceof Manager, lead instanceof Employee, dev instanceof Manager);

Output

Asha earns ₹10,80,000/yr
Kiran earns ₹19,00,000/yr and manages 2
true true false

The prototype chain behind extends

  1. leadname, salary, reports (own fields)
  2. Manager.prototypeannualPay (override), describe
  3. Employee.prototypeannualPay, describe (reached via super)
  4. Object.prototypetoString…

Notice that describe() in Employee calls this.annualPay(), which runs Manager's override for a manager – that's polymorphism: the same call behaves according to the object's actual class.

class Shape { constructor(name) { this.name = name; } }
console.log(typeof Shape, Object.getPrototypeOf(new Shape("x")) === Shape.prototype);
try { Shape("x"); } catch (e) { console.log(e.message); }

Output

function true
Class constructor Shape cannot be invoked without 'new'

A class is a function with prototype-based methods – but safer: it must be called with new, and its body always runs in strict mode.

Prefer composition for "has-a"

Design tip

Use inheritance for a true "is-a" relationship (a Manager is an Employee) and keep hierarchies shallow. For "has-a" or "can-do" behaviour, compose objects: give a class a logger or paymentGateway it delegates to, instead of inheriting from a Logger class.

class Notifier {
  constructor(channel) { this.channel = channel; }
  send(msg) { return `[${this.channel}] ${msg}`; }
}
class OrderService {
  constructor(notifier) { this.notifier = notifier; }      // has-a
  place(id) { return this.notifier.send(`Order ${id} placed`); }
}
console.log(new OrderService(new Notifier("SMS")).place(101));
console.log(new OrderService(new Notifier("Email")).place(102));

Output

[SMS] Order 101 placed
[Email] Order 102 placed

Practice

Try it yourself

  1. Write a Stack class with a private array and push, pop, peek and a size getter.

    Show solution
    class Stack {
      #items = [];
      push(x) { this.#items.push(x); return this; }
      pop() { return this.#items.pop(); }
      peek() { return this.#items.at(-1); }
      get size() { return this.#items.length; }
    }
    const s = new Stack().push("a").push("b").push("c");
    console.log(s.pop(), s.peek(), s.size);

    Output

    c b 2

Quick check

What makes a field truly private?

The # prefix – it can't be accessed outside the class body at all.

Why must a subclass constructor call super() before using this?

The parent constructor creates and initialises the object; this doesn't exist until it runs.

Are classes a new object model in JavaScript?

No – they're syntax over prototypes and constructor functions, with stricter rules.

Summary

  • Classes bundle a constructor, methods, getters/setters and static members.
  • #private fields give real encapsulation; setters validate input.
  • extends/super inherit and override; prefer composition for has-a relationships.

Interview practice: tricky JavaScript questions on async, closures and this.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links