this, Prototypes and Classes · Lesson 23 of 40
JavaScript Classes and Inheritance
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); }
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); }
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));
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);
Asha earns ₹10,80,000/yr Kiran earns ₹19,00,000/yr and manages 2 true true false
The prototype chain behind extends
- leadname, salary, reports (own fields)
- Manager.prototypeannualPay (override), describe
- Employee.prototypeannualPay, describe (reached via super)
- 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); }
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));
[SMS] Order 101 placed [Email] Order 102 placed
Practice
Try it yourself
Write a
Stackclass with a private array andpush,pop,peekand asizegetter.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);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.
#privatefields give real encapsulation; setters validate input.extends/superinherit 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.