1 — Building on an Existing Class
Say you have a Vehicle class with shared behaviour, and now you need a Car and a Motorcycle that both have that behaviour plus something extra. Copying Vehicle's code into both new classes works, but now a bug fix has to happen in three places instead of one.
extends lets one class inherit everything from another: its properties, its methods, its whole shape. The class doing the inheriting is the subclass (or child class); the one being inherited from is the superclass (or parent class).
class Vehicle { constructor(brand) { this.brand = brand; } describe() { return `A ${this.brand} vehicle`; } } class Car extends Vehicle { // nothing added yet — Car already has brand AND describe() for free } const car = new Car('Toyota'); console.log(car.describe()); // "A Toyota vehicle" — inherited, not rewritten console.log(car instanceof Vehicle); // true console.log(car instanceof Car); // true — also itself
Under the hood, this is exactly the prototype chain from Session 03 — just one link longer. Car.prototype's own prototype is now Vehicle.prototype, so a lookup for describe() on a Car instance walks up the chain exactly the way it did before.
2 — super() in the Constructor
The moment your subclass needs its own constructor — to accept extra parameters, say — you must call super() before you can use this. super() runs the parent class's constructor, which is what actually sets up the inherited properties.
class Vehicle { constructor(brand) { this.brand = brand; } } class Car extends Vehicle { constructor(brand, doors) { super(brand); // MUST come first — runs Vehicle's constructor this.doors = doors; // now safe to use `this` for Car's own property } } const car = new Car('Honda', 4); console.log(car.brand); // "Honda" — set by Vehicle's constructor via super() console.log(car.doors); // 4 — set by Car's own constructor // Try this instead, without calling super() first: class Broken extends Vehicle { constructor(brand) { this.brand = brand; // ReferenceError: Must call super constructor first } }
Why the strict ordering? In a subclass constructor, this doesn't exist yet until the parent constructor runs and initializes it. JavaScript enforces this with a hard error rather than letting you use an uninitialized object — one of the few places JS is stricter than you might expect.
One shortcut worth knowing: if your subclass doesn't need its own constructor at all (like Car in Topic 1), you can simply omit it — JavaScript generates an implicit one that just calls super(...args) for you.
3 — Overriding Methods & super.method()
A subclass can redefine a method it inherited — this is called overriding. Sometimes you want to replace the parent's behaviour entirely; other times you want to extend it — run the parent's version, then add something on top. That's what super.method() is for.
class Vehicle { constructor(brand) { this.brand = brand; } describe() { return `A ${this.brand} vehicle`; } } class Car extends Vehicle { constructor(brand, doors) { super(brand); this.doors = doors; } describe() { // OVERRIDE — completely replaces Vehicle's version return `${super.describe()}, ${this.doors}-door`; // ^ calls Vehicle's describe() first, then extends the result } } const car = new Car('Mazda', 2); console.log(car.describe()); // "A Mazda vehicle, 2-door"
Without super.describe() inside the override, you'd have to retype the parent's whole sentence to extend it. With it, the override builds directly on the parent's existing logic — change Vehicle.describe() once, and every subclass override that calls super.describe() picks up the change automatically.
4 — When Not to Use Inheritance
Inheritance models an "is-a" relationship: a Car is a Vehicle. That's a clean fit. Problems start when people reach for extends for things that are really a "has-a" relationship instead.
// "A Car HAS an Engine" — not "a Car IS an Engine" class Engine { start() { return 'Engine running'; } } // WRONG — this says a Car IS an Engine, which doesn't make sense class Car extends Engine { drive() { return 'Driving...'; } } // Now Car is permanently locked into being a kind of Engine. // What happens when you add an ElectricCar with no traditional engine at all? // BETTER — composition: a Car HAS an engine as a property class Car { constructor() { this.engine = new Engine(); // composed, not inherited } drive() { return `${this.engine.start()}, driving...`; } }
The fixed version reads naturally as English: a car has an engine. It's also far more flexible — swap in a different engine object, or none at all for an ElectricCar, without touching the inheritance tree at all. A useful rule of thumb: if you can't honestly say "X is a Y" out loud about your classes, reach for composition (an object holding another object as a property) instead of extends. Session 09 covers composition patterns in depth — for now, just recognize the warning sign.
Long inheritance chains cause a related problem: a change to a class three levels up can silently break a class three levels down that nobody remembers is connected. Favor shallow, well-justified "is-a" hierarchies over deep ones.
15-Minute Review — Session 05
Stop the session timer and switch to the review timer in the sidebar. Answer these questions from memory — then check.
Q1 — What does class Car extends Vehicle give the Car class?
Q2 — In a subclass constructor that defines its own logic, what happens if you use this before calling super()?
Q3 — Inside an overridden method, what does calling super.describe() do?
Q4 — A Car needs an Engine. Why is "a Car has an Engine" (composition) usually better than "a Car extends Engine" (inheritance)?