Module 4 · Objects, Classes & Data Structures ⏱ 19 min

Classes & Inheritance

By the end of this lesson you will be able to:
  • Define a class with a constructor and methods using class syntax
  • Build a subclass with extends and call the parent through super
  • Override a method while reusing the parent's version, and respect the super-before-this rule

Constructor functions and prototypes work, but they spread one idea across two places — the function for setup, its prototype for shared methods — and the wiring is easy to get wrong. Modern JavaScript keeps all of that machinery but gives it a single, tidier shape: the class.

A class is not a new object system grafted onto the language; it is a cleaner coat of paint over the constructor-plus-prototype pair you just learned. The same hidden chain, the same this, the same new. What it adds is readable syntax for that pattern, plus first-class keywords — extends and super — for describing how one kind of object builds on another. Learn the keywords here and you can read most of the JavaScript written today.

flowchart TD
  CL["class Counter"] --> CT["constructor()"]
  CL --> M1["inc()"]
  CL --> M2["reset()"]
  CT -->|"under the hood"| P["Counter.prototype"]
  style CL fill:#e0900b,color:#fff
  style P fill:#3776ab,color:#fff
A class bundles a constructor and its methods into one block; under the hood they land on the prototype.

Anatomy of a class

A class has a special function called the constructor, which runs once per instance when you write new, and any number of methods below it. Everything sits inside one block, which is what makes it easier to scan than the prototype form:

class Counter {
  constructor() { this.count = 0; }   // runs on `new Counter()`
  inc()   { this.count += 1; return this.count; }
  reset() { this.count = 0; }
}

Methods written this way land on Counter.prototype automatically, so they are shared — one copy, reached through the chain, exactly as before. And you still create instances with new: const c = new Counter(). The capital-letter convention carries over, because a class is still a constructor at heart.

A class bundles state and behaviour; new makes an instance. Press Run.
class Counter {
  constructor() { this.count = 0; }
  inc() { this.count += 1; return this.count; }
  reset() { this.count = 0; }
}

const c = new Counter();
console.log(c.inc());
console.log(c.inc());
c.reset();
console.log(c.inc());

Inheritance with extends and super

Inheritance lets one class adopt another's behaviour and then specialise it. Write class Dog extends Animal and Dog automatically gains every method Animal has, because Dog's instances are linked to Animal.prototype through the chain. You only write what is new or different.

To set up the inherited parts, a subclass constructor calls super(...) — the parent's constructor — passing along whatever arguments it needs. That call does double duty: it runs the parent's setup and it is what creates the shared object your subclass will add to. A subclass can also call super.method() to reach the parent's version of a method it has overridden, which is how you extend behaviour instead of replacing it wholesale.

flowchart TD
  S["class Dog extends Animal"] -->|"prototype links to"| A["Animal.prototype"]
  S -->|"super(name) calls"| AC["Animal constructor"]
  style S fill:#1e7f3d,color:#fff
  style A fill:#3776ab,color:#fff
extends links the subclass to its parent; super(name) runs the parent constructor, super.method() reaches an overridden method.
Dog extends Animal, calls super(name) in its constructor, and overrides speak().
class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
  constructor(name, trick) {
    super(name);
    this.trick = trick;
  }
  speak() { return `${this.name} barks and does a ${this.trick}`; }
}

const d = new Dog("Rex", "spin");
console.log(d.speak());
console.log(d.name);

Overriding while reusing the parent

When a subclass defines a method with the same name as the parent's, its version overrides the inherited one — calls on a Dog reach Dog.prototype.speak, not Animal's. But you rarely want to throw the parent's work away. By calling super.speak() from inside the override, you run the parent's method first and then add to its result, so behaviour composes instead of clashing.

This is the pattern behind most real inheritance: a thin layer that delegates the heavy lifting up the chain and tacks on a specialisation. Reach for super.method() whenever an override should extend rather than replace, and you keep the parent's guarantees intact while still customising the output.

flowchart TD
  O["SuperHero.greet() runs"] -->|"shadows"| PA["Hero.greet()"]
  O -->|"super.greet() reaches it"| PA
  style O fill:#e0900b,color:#fff
  style PA fill:#3776ab,color:#fff
An override shadows the parent method on the subclass; super.method() reaches past the shadow to the original.
An override that builds on the parent: super.greet() does the base work, then the subclass adds to it.
class Hero {
  constructor(name) { this.name = name; }
  greet() { return `I am ${this.name}`; }
}

class SuperHero extends Hero {
  greet() {
    return super.greet() + ", here to help!";
  }
}

const h = new SuperHero("Ada");
console.log(h.greet());
Exercise

What does this print? toggle flips this.on and returns the new value.

class Light {
  constructor() { this.on = false; }
  toggle() { this.on = !this.on; return this.on; }
}
const l = new Light();
console.log(l.toggle());
console.log(l.toggle());
Exercise

Write a class Counter whose constructor sets this.count to 0, with a method inc() that adds 1 to this.count and returns the new value. Use class syntax with a constructor and a method.

class Counter {
  // constructor sets this.count = 0
  // inc() adds 1 and returns the new count
}
Exercise

Dog extends Animal, but the constructor reads this.breed before calling super(name) — so it throws ReferenceError: Must call super constructor in derived class before accessing 'this'. Reorder the constructor so the parent is initialised first, then this is used.

class Animal {
  constructor(name) { this.name = name; }
  describe() { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
  constructor(name) {
    this.breed = "mutt";   // BUG: this used before super()
    super(name);
  }
}

const d = new Dog("Rex");
Exercise

The starter gives you a BankAccount class (do not change it). Write BonusAccount extends BankAccount: its constructor takes a starting balance and calls super(starting). Override deposit(amount) so it pays a 10% bonus — call super.deposit(amount) to bank the main amount, then add amount * 0.1 to this.balance, and return this.balance. So depositing 100 onto a 0 balance leaves 110.

class BankAccount {
  constructor(starting) { this.balance = starting; }
  deposit(amount) { this.balance += amount; return this.balance; }
}

// Write BonusAccount extends BankAccount below
Exercise

Inside a subclass constructor, what happens if you read this before calling super()?

Recap

  • A class bundles a constructor and methods into one readable block; under the hood it is still constructor-plus-prototype, and you still instantiate with new.
  • Methods written inside a class are shared on the prototype automatically — one copy per class.
  • extends makes a subclass adopt a parent's methods through the prototype chain; super(...) runs the parent constructor.
  • Overriding a method shadows the parent's; call super.method() to reuse the parent version and extend it instead of replacing it.
  • In a subclass constructor, super() must run before this is used, or a ReferenceError is thrown.

Next you'll meet the modern collectionsMap and Set — built for the jobs where a plain object is the wrong tool.

Checkpoint quiz

Why does this subclass constructor throw — constructor(n) { this.x = 1; super(n); }?

How do you call the parent's version of a method you have overridden?

Go deeper — technical resources