Chapter 11 ยท Collections & Objects

Classes & the Prototype Chain

ES6 classes look like Java classes, but underneath they're built on JavaScript's original model: objects linked to other objects through a prototype chain. This chapter covers the full modern class syntax, including private #fields and static blocks, and then opens the hood.

You will learn

  • Class syntax: constructor, methods, getters and setters, static members ES2015.
  • Public and private fields, private methods, #x in obj, static blocks ES2022.
  • extends, super, overriding, and extending built-ins like Error.
  • The prototype chain: how property lookup works, and what a class compiles to.
  • this in class methods, arrow-function fields, and mixins.

Class syntax

Facts about classes that surprise Java developers:

Private members ES2022

#name is truly private: it's a syntax error to mention it outside the class body, and it doesn't show up in any reflection (Object.keys, JSON, Reflect.ownKeys). Methods and accessors can be private too. #field in obj checks whether an object has that private field, which is a safe brand check.

๐Ÿ“ Older syntax you'll still see: the _balance underscore convention ("please don't touch"), closures, or WeakMaps keyed by instance for privacy. TypeScript's private keyword only exists at compile time, unlike #.

Static initialization blocks ES2022

Inheritance with extends

โš ๏ธ Pitfall: in a subclass constructor, using this before calling super() throws ReferenceError: Must call super constructor in derived class before accessing 'this'.

Extending built-ins

Under the hood: the prototype chain

Every object has a hidden link, [[Prototype]], to another object (or to null). When you read a property that the object doesn't have itself, JavaScript follows the link and looks there, then follows that object's link, and so on. This lookup path is the prototype chain.

What a class compiles to

Before ES6, the same structure was written with a constructor function and its prototype object. Classes are (mostly) nicer syntax for exactly this; Appendix A shows more.

ES5: constructor function

ES6 class

The differences: class methods are non-enumerable, classes can't be called without new, they're strict, they have real #private members, and super works.

โ˜• Java: there are no separate "class" and "instance" kinds of thing at run time: a class is an object (a function), and inheritance links objects, not types. You can even create an object with a given prototype directly, with Object.create(proto), with no class involved.

this in class methods

Class methods are ordinary functions on the prototype, so they lose this when they're detached (Chapter 4). A class field holding an arrow function is created per instance, and captures that instance's this, which is a common fix for event handlers.

The trade-off: arrow fields aren't on the prototype, so each instance gets its own copy, and subclasses can't call them with super.

Mixins

JavaScript has single inheritance. To share behaviour across unrelated classes, use a mixin: a function that takes a base class and returns a subclass with extra methods.

More examples

1. A small class hierarchy for discounts

2. Validating setters with a private backing field

3. Objects without classes: Object.create

4. Static factory methods with validation

5. A domain error hierarchy

new.target refers to the class that was actually instantiated, so name is right for every subclass without repeating it.

Recap

  • A class is a strict, non-hoisted, new-only function. Methods live on the prototype; fields live on each instance.
  • #private fields and methods are truly hidden; #x in obj is a brand check; static blocks initialise static state.
  • extends + super() (before this) + super.method(); built-ins like Error and Array can be extended.
  • Property lookup follows the [[Prototype]] chain, and own properties shadow inherited ones. Classes are mostly syntax over constructor functions.
  • Detached methods lose this; arrow-function fields bind it per instance. Use mixins for shared behaviour.

Exercises

Exercise 1 ๐ŸŸข easy build it

Write a class Rect with width and height, a getter area, and a static method square(side).

Show solution

Getters are read without parentheses; static methods are called on the class.

Exercise 2 ๐ŸŸข easy predict the output

What does typeof class {} give? And what happens with class A {}; A();?

Show solution

"function"; and TypeError: Class constructor A cannot be invoked without 'new'.

Exercise 3 ๐ŸŸข easy fix the bug

This throws a ReferenceError. Fix it.

Show solution

In a derived class, this doesn't exist until super() has created and initialised it.

Exercise 4 ๐ŸŸข easy explain why

Why isn't describe listed by Object.keys(new Book("Dune")), even though dune.describe() works?

Show solution

Methods live on Book.prototype, not on the instance, and they're non-enumerable. Object.keys lists only own enumerable properties; the call works because lookup follows the prototype chain.

Exercise 5 ๐ŸŸก medium predict the output

What does this print?

Show solution

own B>A true. The own property shadows the method. C.prototype.greet is found on B.prototype, and it calls A's version via super.

Exercise 6 ๐ŸŸก medium refactor it

Convert this ES5 code to a class with a private field.

Show solution

#items is genuinely private, unlike the _items convention, and a getter reads naturally.

Exercise 7 ๐ŸŸก medium build it ยท playground

Create playground/models.js with a Book class (id, title, price in cents, private stock with reserve(qty) and release(qty)), and an OutOfStockError that extends Error. reserve throws OutOfStockError when there isn't enough stock.

Show solution

Stock is private, so it can only change through reserve and release, and it doesn't leak into the JSON. Chapter 16 reuses this class.

Exercise 8 ๐ŸŸก medium fix the bug

The button handler (simulated) throws. Fix it inside the class.

Show solution

Alternatively, keep the method and bind it in the constructor: this.onClick = this.onClick.bind(this).

Exercise 9 ๐Ÿ”ด hard predict the output

What does this print?

Show solution

Base sees base Derived.describe. When Base's constructor runs, Derived's fields haven't been initialised yet (that happens after super() returns), so name is still "base". But methods come from the prototype chain, which is already Derived's. Java behaves similarly with overridden methods called from constructors.

Exercise 10 ๐Ÿ”ด hard mini-project ยท playground

Create playground/events.js with an EventEmitter class: private listener storage (a Map of Sets), on(event, fn) returning an unsubscribe function, once, and emit(event, ...args) returning the number of listeners called. Then make an OrderBook extends EventEmitter that emits "added" when an order is placed.

Show solution

It combines private fields, closures for unsubscribe, once built on on, inheritance, and a defensive copy while emitting. Node's own node:events module works the same way.

Official docs