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 likeError.- The prototype chain: how property lookup works, and what a class compiles to.
thisin class methods, arrow-function fields, and mixins.
Class syntax
Facts about classes that surprise Java developers:
- A class is a special function:
typeof Book === "function". It must be called withnew. - Class bodies are always in strict mode.
- Class declarations are not hoisted like functions: they're in the TDZ until their line runs.
- Fields are initialised before the constructor body runs (in a subclass, right after
super()returns). - There's no overloading and no access modifiers other than
#.
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. #privatefields and methods are truly hidden;#x in objis a brand check; static blocks initialise static state.extends+super()(beforethis) +super.method(); built-ins likeErrorandArraycan 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.