Classes, Inheritance and Private Fields

Classes are syntax over prototypes. This lesson teaches the syntax and the machinery underneath, because the bugs come from the machinery.

Classes organize prototypes

The syntax is sugar; the behaviour is real

A class bundles what lesson 09 taught — constructor + shared prototype methods — into one readable block. Instances are created with new:

class Timer {
  #seconds = 0;                    // PRIVATE field: only class code can touch it

  constructor(label) {             // runs once per `new`
    this.label = label;            // public field: lives on the instance
  }

  tick() {                         // a method: shared via the prototype
    this.#seconds += 1;
    return this.#seconds;
  }

  get seconds() {                  // getter: reads like a property
    return this.#seconds;
  }

  static fromSeconds(label, s) {   // static: belongs to the CLASS, not instances
    const timer = new Timer(label);
    for (let i = 0; i < s; i += 1) timer.tick();
    return timer;
  }
}

const timer = Timer.fromSeconds("tea", 3);
console.log(timer.seconds);        // 3
console.log(timer.tick());         // 4
// console.log(timer.#seconds);    // SyntaxError — #private is private, even outside

You should see: 3, then 4. The private field keeps seconds read-only from outside — no underscore conventions, enforced by the engine.

Inheritance

extends and super

class BreakTimer extends Timer {        // BreakTimer IS-A Timer
  constructor(label, pauseSeconds) {
    super(label);                       // MUST come first: runs Timer's constructor
    this.pauseSeconds = pauseSeconds;
  }
  describe() {
    // super.xxx also reaches the parent's methods; statics via the parent name
    return `${this.label}: ${this.seconds}s (+${this.pauseSeconds}s pause)`;
  }
}

const t = new BreakTimer("focus", 30);
t.tick();
console.log(t.describe());       // focus: 1s (+30s pause)
console.log(t instanceof Timer); // true

If you get this instead: ReferenceError: must call super constructor before using 'this' — you used this before super(...); the parent builds the instance first.

this: the four binding rules

The call site decides

Diagram: four this-binding rules — method call binds to the object, plain call to undefined in strict mode, arrow inherits from birth scope, new binds to the fresh instance

Four rules, decided by HOW the function is called — the arrow rule being the escape hatch.

const counter = {
  value: 10,
  show() { return `value is ${this.value}`; },   // rule 1: this = counter
  later() {
    setTimeout(() => console.log(`kept: ${this.value}`), 50);
    // rule 3: the ARROW inherited this from later()'s scope — the fix
  },
};
console.log(counter.show()); // value is 10
counter.later();             // (50 ms later) kept: 10

// THE TRAP: extract the method, and rule 2 applies at the new call site.
const extracted = counter.show;   // no dot anymore!
// extracted();                   // TypeError: Cannot read properties of undefined

"Lost my this" is always rule 2 in disguise: the function ran without an object. The fixes, in order of preference: keep the call as a method call; use an arrow (inherits); or fn.bind(obj) to pin this permanently.

Composition over deep hierarchies

When to stop subclassing

Inheritance is for genuine is-a relationships that share behaviour, not for reusing two lines. Deep hierarchies (Admin → User → Person → Base) couple everything to everything. Prefer composing small abilities:

const canSerialize = {
  toJSON() { return JSON.stringify(this); },
};
const canLog = {
  log(...args) { console.log(`[${this.constructor.name}]`, ...args); },
};
class Session {
  constructor(user) { Object.assign(this, canSerialize, canLog, { user }); }
}
new Session("ada").log("started"); // [Session] started

Mixins add abilities without a parent chain — the same idea the DOM and event systems use.

Practice: a tiny class family

The task

Run foundations/18_classes_lab.js with node and predict each output. Then write class Countdown extends Timer where tick() counts down (call super only if you need the parent), and explain what happens when it reaches zero.

The checklist

  • You can explain what #private adds over a plain field.
  • You know why super(...) must precede this in a subclass constructor.
  • You can name the four this rules and spot the "lost this" trap in code.
  • You can say when inheritance is the right tool — and when composition is.