Classes

Updated 7 interview angles 6 min read source
On this page9
  1. What class actually creates
  2. Fields, and why order matters
  3. #private is real privacy
  4. Inheritance and super
  5. Static, and what this means in it
  6. Mixins, because there is no multiple inheritance
  7. When not to use a class
  8. Related
  9. Interview angle

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

js
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

js
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:

js
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

js
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, and JSON.stringify silently drops all of them — which surprises people who expected _balance behaviour.

Inheritance and super

js
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:

js
Object.getPrototypeOf(Loud.prototype) === Base.prototype;  // instances
Object.getPrototypeOf(Loud) === Base;                      // statics inherit too

That second line is why a static method on Base is callable as Loud.method().

Static, and what this means in it

js
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:

js
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:

js
class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.name = "HttpError";
    this.status = status;
  }
}

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 #private different from an underscore convention?” - it is enforced by the language. Accessing it from outside is a SyntaxError at parse time, and the field is invisible to Object.keys and JSON.stringify. A subclass cannot reach the parent’s private fields either.
  • “Why must super() come before this?” - the instance does not exist until the parent constructor has run. Touching this first is a ReferenceError, and omitting super() 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 after super(), which is why calling overridable methods from a constructor is a bad idea.
  • “Why write new this() in a static factory?” - this in a static method is the class it was called on, so a subclass calling the inherited factory gets an instance of itself. Hardcoding new 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 instanceof and super keep 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 avoid this entirely.