Class Inheritance & Private Fields
A class can extend another class to reuse its code and add or change behaviour. And with #private fields and methods, a class can hide its internal details so outside code can only use the public interface you choose.
extends and super
class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound`; } } class Dog extends Animal { constructor(name, breed) { super(name); this.breed = breed; } speak() { return `${this.name} barks`; } describe() { return `${super.speak()} — specifically, ${this.speak()}`; } } const rex = new Dog("Rex", "collie"); console.log(rex.speak()); console.log(rex.describe()); console.log(rex instanceof Animal);
Rex barks Rex makes a sound — specifically, Rex barks true
class Dog extends AnimalmakesDoginherit every method ofAnimal.- In a subclass constructor,
super(...)calls the parent constructor. It must run before you usethis. - Defining a method with the same name overrides the parent version.
super.method()calls the parent's version.
If a subclass has no constructor, it automatically gets one that passes all arguments to super.
Forgetting super
class Base { constructor() { this.ready = true; } } class Child extends Base { constructor() { this.extra = 1; } } new Child();
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
Private fields and methods
Names starting with # are truly private: they can only be used inside the class body. Outside code can't read or even detect them:
class BankAccount { #balance = 0; #history = []; constructor(owner) { this.owner = owner; } deposit(amount) { this.#validate(amount); this.#balance += amount; this.#history.push(`+${amount}`); } withdraw(amount) { this.#validate(amount); if (amount > this.#balance) throw new Error("Insufficient funds"); this.#balance -= amount; this.#history.push(`-${amount}`); } get balance() { return this.#balance; } #validate(amount) { if (!(amount > 0)) throw new Error("Amount must be positive"); } } const acc = new BankAccount("Lee"); acc.deposit(100); acc.withdraw(30); console.log(acc.balance); console.log(acc.balance = 1_000_000, acc.balance); console.log(Object.keys(acc));
70 1000000 70 [ 'owner' ]
The only way to change the balance is through deposit and withdraw, which validate every change. Assigning to balance has no effect because there's only a getter (in strict mode, including modules, it throws a TypeError instead), and the private fields don't appear in Object.keys. Writing acc.#balance outside the class is a syntax error.
Static inheritance and private static
class Shape { static #count = 0; constructor() { Shape.#count++; } static get count() { return Shape.#count; } } class Circle extends Shape {} class Square extends Shape {} new Circle(); new Square(); new Circle(); console.log(Shape.count, Circle.count);
3 3
Static members are inherited too: Circle.count works because Circle's prototype chain includes Shape.
Extending built-in classes
You can extend built-ins like Error, Array or Map. Custom error classes are the most useful example (lesson 54):
class ValidationError extends Error { constructor(field, message) { super(message); this.name = "ValidationError"; this.field = field; } } try { throw new ValidationError("email", "Email is required"); } catch (err) { console.log(err instanceof ValidationError, err.name, err.field, err.message); }
true ValidationError email Email is required
Composition over inheritance
Inheritance models an "is a" relationship: a dog is an animal. Deep inheritance trees get rigid and hard to follow, though. Often it's better to build objects from smaller parts ("has a"):
class Logger { log(msg) { console.log(`[log] ${msg}`); } } class OrderService { constructor(logger) { this.logger = logger; } place(item) { this.logger.log(`order placed: ${item}`); } } new OrderService(new Logger()).place("book");
[log] order placed: book
Passing collaborators in like this also makes classes easy to test — you can swap in a fake logger.