Classes, Inheritance and Private Fields
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
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
#privateadds over a plain field. - You know why
super(...)must precedethisin a subclass constructor. - You can name the four
thisrules and spot the "lost this" trap in code. - You can say when inheritance is the right tool — and when composition is.