JavaScript · Core language
Classes & inheritance
⚡ TL;DR —
classis sugar over prototypes, but not only sugar — it adds semantics you can't cleanly hand-roll:super, non-enumerable methods, mandatorynew, true#privatefields, and a TDZ on the class binding. Inheritance wires up two chains at once — instances up the prototype chain, statics up the constructor chain.
🧠 Mental model
A class declaration builds exactly the constructor-function-plus-prototype you'd write by hand: instance methods live on Class.prototype, static members on the constructor itself, instance fields get set per object in the constructor. extends then links two chains simultaneously — Child.prototype's prototype becomes Parent.prototype (so instances inherit methods) and Child's prototype becomes Parent (so statics inherit too). Understanding that dual wiring is the senior signal.
⚙️ How it actually works
Class bodies always run in strict mode. Methods defined in the body are non-enumerable (unlike Foo.prototype.m = …, which is enumerable). Calling a class without new throws — a deliberate guardrail constructor functions lack.
The load-bearing rule is super. In a derived constructor, the parent constructor is what actually creates this, so you must call super(...) before touching this — before that, this sits in the temporal dead zone and any access throws. super.method() looks the method up on the parent prototype but calls it with the current this, so overrides can extend rather than replace behaviour.
💻 Code
class Animal {
#energy = 100; // truly private, per-instance
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
static create(n) { return new this(n); } // `this` = the class it's called on
}
class Dog extends Animal {
constructor(name) {
super(name); // MUST run before using `this`
this.legs = 4;
}
speak() { return `${super.speak()} — a bark`; } // super keeps `this` = the Dog
}
The TDZ trap:
class Cat extends Animal {
constructor(name) {
this.name = name; // 💥 ReferenceError — `this` used before super()
super(name);
}
}
⚖️ Trade-offs
- Classes are the right call for stateful entities — readable construction, real privacy via
#, clear intent, non-enumerable methods that don't pollute iteration. - But inheritance is the weakest form of reuse. Deep hierarchies are rigid and couple subclasses to parent internals. For sharing behaviour, prefer composition — mixins, plain functions, or injected collaborators.
- Arrow methods as class fields auto-bind
this(handy for handlers) but cost a per-instance allocation, sit on the instance not the prototype, and can't usesuperor be overridden via the prototype — a real testing/override friction.
💣 Gotchas interviewers probe
- Classes are hoisted but in the TDZ.
new Foo()beforeclass Foo {}throws — unlike function declarations, which are fully hoisted. super()beforethisin every derived constructor, or ReferenceError.- Methods are non-enumerable.
Object.keys(instance)andfor…inskip them, whereas hand-rolled prototype methods show up. This surprises people writing generic serializers. - Static members are inherited too.
Child.create()works becauseChild's prototype isParent. Many candidates think statics don't inherit. - Arrow methods aren't on the prototype — no
super, one per instance, invisible to prototype-based spies. - Forgetting
newthrows — a feature, not a bug (constructor functions silently misbehaved instead). extendstakes an expression and can even extendnull; extending built-ins likeArrayworks but has historical engine quirks.
🎯 Say this in the interview
"
classis sugar over the prototype system, but it adds semantics I'd struggle to reproduce by hand: methods are non-enumerable, calling withoutnewthrows,#fields are genuinely private, and the class binding is in a TDZ so I can't use it before its declaration. When I useextends, two chains get wired up — instances inherit methods up the prototype chain, and the subclass inherits static members up the constructor chain. The rule I never violate is callingsuper()before touchingthisin a derived constructor, because the parent constructor is what actually allocatesthis; using it earlier throws. Design-wise I reach for classes for stateful entities, but for sharing behaviour I lean on composition, because deep inheritance couples subclasses to parent internals and gets brittle fast."
🔗 Go deeper
- javascript.info — Class basic syntax — what the sugar desugars to.
- javascript.info — Class inheritance —
extends,super, and the dual-chain wiring. - javascript.info — Private and protected properties —
#fields versus the older conventions. - MDN — Classes — full reference including static blocks and field semantics.