Classes
Sugar over the prototype chain, with a few things that are genuinely new. The interview value is knowing which is which — because the sugar has semantics the old syntax did not, and the new parts are the ones people get wrong.
The machinery underneath is Prototypes and the Prototype Chain.
What class actually creates
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
// goes on Point.prototype
norm() {
return Math.hypot(this.x, this.y);
}
}
typeof Point; // "function"
Object.getOwnPropertyNames(Point.prototype); // ["constructor", "norm"]A class is a function, and methods land on its prototype — exactly where
they would have gone if you had written a constructor function by hand. The
sugar is that instance methods are non-enumerable by default, so a for...in
over an instance no longer walks them.
Three behaviours are not sugar:
| Behaviour | Class | Constructor function |
|---|---|---|
| Hoisting | in the temporal dead zone | hoisted and callable |
Called without new |
TypeError |
runs, this is undefined or global |
| Body strict mode | always | only if the file is |
The second is the practical one. Point(1, 2) throws instead of silently
polluting the global object, which is a real class of bug the old syntax had.
Fields, and why order matters
class Counter {
count = 0; // instance field
static registry = new Map(); // static field
#secret = 42; // truly private
#bump() { this.#secret++; } // private method
static { // static init block
Counter.registry.set("created", 0);
}
}Instance fields are assigned in declaration order, before the constructor
body runs — and after super() in a subclass. That ordering is what makes
this fail:
class A {
// subclass override runs...
constructor() { this.init(); }
}
class B extends A {
// ...before this is assigned
value = 1;
init() { console.log(this.value); } // undefined
}
new B();The parent constructor calls an overridden method before the child’s fields exist. It is the JavaScript version of a well-known C++/Java trap, and it is why calling overridable methods from a constructor is a bad idea in any language.
#private is real privacy
class Account {
#balance = 0;
deposit(n) { this.#balance += n; }
get balance() { return this.#balance; }
// Only works because the field is on the same class.
static isAccount(o) { return #balance in o; }
}
const a = new Account();
a.#balance; // SyntaxError, at parse time
Object.keys(a); // [] — invisible to reflection
JSON.stringify(a); // "{}"Unlike the _underscore convention or a closure, this is enforced by the
language: not reachable, not enumerable, not serialisable. The #x in obj
form is the sanctioned brand check — it is how you ask “is this really one of
mine” without a try/catch.
Gotcha: private fields are per-class, not per-instance-hierarchy. A subclass cannot touch the parent’s
#field, andJSON.stringifysilently drops all of them — which surprises people who expected_balancebehaviour.
Inheritance and super
class Base {
constructor(name) { this.name = name; }
greet() { return `hi ${this.name}`; }
}
class Loud extends Base {
constructor(name) {
// must come before any `this`
super(name);
this.volume = 11;
}
greet() {
// call the overridden one
return super.greet().toUpperCase();
}
}super() before this is a hard rule: the instance does not exist until
the parent constructor has run, so touching this first is a ReferenceError.
Omitting super() entirely in a subclass constructor is the same error.
extends wires two chains, which is the thing to be able to say:
Object.getPrototypeOf(Loud.prototype) === Base.prototype; // instances
Object.getPrototypeOf(Loud) === Base; // statics inherit tooThat second line is why a static method on Base is callable as Loud.method().
Static, and what this means in it
class Repo {
static #cache = new Map();
static create(id) {
// `this` is the class — subclass-aware
return new this(id);
}
}
class UserRepo extends Repo {}
// true, because of `new this`
UserRepo.create(1) instanceof UserRepo;new this() rather than new Repo() is what makes a static factory work
correctly under inheritance. It is the small detail that separates knowing
statics from having used them.
Mixins, because there is no multiple inheritance
A mixin is a function from a class to a class:
const Serializable = (Base) => class extends Base {
toJSON() { return { ...this }; }
};
const Comparable = (Base) => class extends Base {
equals(o) { return this.id === o.id; }
};
class Model extends Serializable(Comparable(Object)) {}Each call inserts a link in the prototype chain, so instanceof still behaves
and super still works through the stack. It composes where extends cannot,
and the cost is a deeper chain and a harder-to-read hierarchy.
When not to use a class
Most JavaScript does not need one. A class earns its place when there is
identity plus mutable state plus behaviour over it — a connection pool, a
state machine, a custom Error. For everything else a plain object and
functions are simpler, tree-shake better, and avoid this entirely.
React went from classes to hooks for exactly this reason, and the this
problems in this and Binding are most of why.
Subclassing Error is the one place a class is clearly right:
class HttpError extends Error {
constructor(status, message) {
super(message);
this.name = "HttpError";
this.status = status;
}
}Related
Interview angle 7
- “Are JavaScript classes just syntactic sugar?” - mostly, but not entirely. Methods do land on the prototype exactly as with a constructor function, and three things differ: classes are in the temporal dead zone rather than hoisted, throw if called without
new, and their bodies are always strict mode. - “How is
#privatedifferent from an underscore convention?” - it is enforced by the language. Accessing it from outside is aSyntaxErrorat parse time, and the field is invisible toObject.keysandJSON.stringify. A subclass cannot reach the parent’s private fields either. - “Why must
super()come beforethis?” - the instance does not exist until the parent constructor has run. Touchingthisfirst is aReferenceError, and omittingsuper()in a subclass constructor is the same error. - “A parent constructor calls a method the child overrode. What happens?” - the override runs before the child’s instance fields are assigned, so they are
undefined. Fields initialise in declaration order aftersuper(), which is why calling overridable methods from a constructor is a bad idea. - “Why write
new this()in a static factory?” -thisin a static method is the class it was called on, so a subclass calling the inherited factory gets an instance of itself. Hardcodingnew Base()breaks that. - “How do you get multiple inheritance?” - you do not; you use mixins. A mixin is a function taking a base class and returning a subclass of it, so each one adds a link to the prototype chain and
instanceofandsuperkeep working. - “When would you not use a class?” - most of the time. A class earns its place when there is identity plus mutable state plus behaviour — a pool, a state machine, a custom
Error. Otherwise plain objects and functions are simpler and avoidthisentirely.