Q37 of 40 · JavaScript

What are the practical differences between ES6 `class` and prototype-based patterns, and when does it matter?

JavaScriptSeniorjavascriptclassprototypesoopprivate-fieldsinheritance

Short answer

Short answer: `class` is syntactic sugar over prototypes — methods go on the prototype, instances share them. Practical differences: `class` enforces `new`, supports `super`, and `class` bodies are in strict mode. Private fields (`#field`) are a true encapsulation mechanism not achievable with prototype patterns.

Detail

ES6 class compiles to roughly the same prototype chain structure but with several behavioral differences that matter in practice.

What class adds over manual prototypes:

  1. Mandatory new: Calling a class without new throws TypeError — unlike constructor functions which silently pollute the global object in sloppy mode.
  2. super: class provides clean, spec-compliant super-call semantics for derived class constructors.
  3. Strict mode: Class bodies are always in strict mode.
  4. Private fields: #field syntax provides true encapsulation (not accessible outside the class body), enforced at the language level — not possible with prototype patterns.
  5. Static blocks: static { ... } for complex static initialization.
  6. Non-enumerable methods: Methods defined in class bodies are non-enumerable on the prototype — unlike Foo.prototype.method = function() {} which is enumerable.

When it matters for testing:

  • Private fields cannot be accessed in tests — you must test them through public API, or use --expose-gc + #field accessor workarounds.
  • typeof MyClass is 'function' — it's a function under the hood.
  • Mocking a class method can be done on the prototype: MyClass.prototype.method = jest.fn().

// EXAMPLE

// Prototype pattern (pre-ES6 style)
function Animal(name) { this.name = name; }
Animal.prototype.speak = function() { return `${this.name} speaks`; };

// class — equivalent but cleaner, with extras
class Animal2 {
  #name; // private — not accessible outside
  constructor(name) { this.#name = name; }
  speak() { return `${this.#name} speaks`; }
  get name() { return this.#name; } // public accessor
}

// class methods are non-enumerable
console.log(Object.keys(Animal2.prototype)); // []
// vs prototype assignment: Animal.prototype.speak is enumerable

// Mocking a class method in Jest
jest.spyOn(Animal2.prototype, "speak").mockReturnValue("mocked");

// Private field — not accessible in test
const a = new Animal2("Dog");
// console.log(a.#name); // SyntaxError — truly private

// WHAT INTERVIEWERS LOOK FOR

Non-enumerable methods, mandatory new, private fields as true encapsulation, and strict mode. Connecting to test patterns — mocking via prototype and the limitation of testing private fields. This shows depth beyond 'class is just syntactic sugar'.

// COMMON PITFALL

Thinking private fields (`#field`) are just a convention like `_field` — they are enforced at the language level. A test that tries to access `instance.#field` will throw a SyntaxError, not just return undefined.