Inheritance
Mental model: extends links two chains at once: instances to the parent prototype, and the subclass itself to the parent class. super is how you step back up either one.
Level: advanced · about 16 minutes
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
static describe() { return 'an animal'; }
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // run Animal's constructor first
this.breed = breed;
}
speak() {
return `${super.speak()}, specifically a bark`; // call the overridden method
}
}
const rex = new Dog('Rex', 'collie');
console.log(rex.speak()); // 'Rex makes a sound, specifically a bark'
console.log(rex instanceof Dog, rex instanceof Animal); // true true
console.log(Dog.describe()); // 'an animal' - statics are inherited tooTwo keywords do all the wiring you used to do by hand.
Before extends, you wrote three fiddly lines: Dog.prototype = Object.create(Animal.prototype), then restored constructor, then called Animal.call(this, name) in the body. extends does all of it, plus one thing you could not do by hand: it links Dog itself to Animal, which is why Dog.describe() works.
The two chains
rex ──> Dog.prototype ──> Animal.prototype ──> Object.prototype ──> null
(speak) (constructor,
speak)
Dog ──> Animal ──> Function.prototype ──> Object.prototype ──> null
(static members) (static describe)
extends builds BOTH arrows:
Object.setPrototypeOf(Dog.prototype, Animal.prototype) <- instance methods
Object.setPrototypeOf(Dog, Animal) <- static members
class Base {}
class Sub extends Base {}
console.log(Object.getPrototypeOf(Sub.prototype) === Base.prototype); // true
console.log(Object.getPrototypeOf(Sub) === Base); // true
console.log(Sub.prototype.constructor === Sub); // true
console.log(new Sub() instanceof Base); // trueCheck both links yourself.
super in a constructor
In a derived constructor, this does not exist until super() returns. The parent constructor is what creates the object, so touching this first is a ReferenceError. If you write no constructor at all, you get an implicit constructor(...args) { super(...args); }.
Broken: this before super
class Dog extends Animal {
constructor(name) {
this.legs = 4; // ReferenceError:
super(name); // must call super
} // first
}
Correct: super first, then this
class Dog extends Animal {
constructor(name) {
super(name);
this.legs = 4;
}
}
// No constructor needed at all if
// you only forward arguments.Class fields on a subclass are installed immediately after super() returns, which is also why a field initialiser cannot see values the parent constructor assigns after that point. Keep parent constructors simple.
class Base {
constructor() { this.tag = 'base'; }
}
class Derived extends Base {
extra = 'field'; // installed right after super() returns
constructor() {
super();
console.log(this.tag, this.extra); // 'base' 'field'
}
}
new Derived();
class Forgetful extends Base {
constructor() { /* no super() */ }
}
try { new Forgetful(); } catch (e) { console.log(e.constructor.name); } // ReferenceError
Overriding, and super in methods
A method on the subclass prototype shadows the parent one, because lookup stops at the first match. super.method() skips your own prototype and starts the search one level up, while still passing the current object as this.
class Repo {
constructor() { this.log = []; }
save(item) { this.log.push(`save ${item}`); return item; }
}
class AuditedRepo extends Repo {
save(item) {
this.log.push(`audit ${item}`);
return super.save(item); // same `this`, parent implementation
}
}
const r = new AuditedRepo();
r.save('x');
console.log(r.log); // ['audit x', 'save x']
console.log(Object.getOwnPropertyNames(AuditedRepo.prototype)); // ['constructor', 'save']
class Model {
static table = 'models';
static describe() { return `table=${this.table}`; }
static create(data) { return new this(data); } // `this` is the subclass
constructor(data) { this.data = data; }
}
class User extends Model {
static table = 'users';
static describe() { return `User(${super.describe()})`; }
}
console.log(User.describe()); // 'User(table=users)'
console.log(User.create({ id: 1 }) instanceof User); // true, thanks to `new this`
console.log(Model.describe()); // 'table=models'super also works in statics, and it walks the class chain.
Abstract-ish base classes
JavaScript has no abstract keyword. You get the same effect with two cheap guards: refuse to construct the base directly by comparing new.target, and make unimplemented methods throw. The base can then call those methods, which is the template method pattern.
class Shape {
constructor(name) {
if (new.target === Shape) throw new TypeError('Shape is abstract');
this.name = name;
}
area() { throw new Error(`${this.constructor.name} must implement area()`); }
describe() { return `${this.name}: ${this.area().toFixed(2)}`; }
}
class Circle extends Shape {
constructor(r) { super('circle'); this.r = r; }
area() { return Math.PI * this.r ** 2; }
}
class Blob extends Shape {
constructor() { super('blob'); }
}
console.log(new Circle(2).describe()); // 'circle: 12.57'
try { new Shape('x'); } catch (e) { console.log(e.message); } // 'Shape is abstract'
try { new Blob().describe(); } catch (e) { console.log(e.message); } // 'Blob must implement area()'
Extending built-ins
class Stack extends Array {
peek() { return this[this.length - 1]; }
static get [Symbol.species]() { return Array; } // map/filter give plain Arrays
}
const s = new Stack();
s.push('a', 'b');
console.log(s.peek(), s.length); // 'b' 2
console.log(Array.isArray(s)); // true
console.log(s.map((x) => x) instanceof Stack); // false, because of species
console.log(s instanceof Stack, s instanceof Array); // true trueSubclassing Array keeps the exotic length behaviour and the methods.
class AppError extends Error {
constructor(message, options) {
super(message, options); // forwards { cause }
this.name = new.target.name; // 'AppError', or the subclass name
}
}
class NotFound extends AppError {
constructor(what) {
super(`${what} not found`, { cause: new Error('lookup failed') });
this.status = 404;
}
}
const err = new NotFound('user');
console.log(err.name, err.status); // 'NotFound' 404
console.log(String(err)); // 'NotFound: user not found'
console.log(err instanceof NotFound, err instanceof AppError, err instanceof Error); // true true true
console.log(err.cause.message, typeof err.stack); // 'lookup failed' 'string'The subclass you will actually write most often.
| Detail | What to do | Why |
|---|---|---|
name | set it in the constructor, or use new.target.name | it defaults to "Error", so logs lie |
message | pass it to super(message) | the parent installs it non-enumerably |
cause | pass { cause } as the second argument | ES2022, keeps the original error attached |
stack | let the engine set it | V8 fills it in during super() |
instanceof | works natively | only breaks in output transpiled to ES5 |
| extra data | own properties after super() | a status or field beats string-parsing the message |
class Base {
constructor() { this.value = this.init(); }
init() { return 'base'; }
}
class Sub extends Base {
suffix = '!';
init() { return 'sub' + this.suffix; }
}
console.log(new Sub().value);The parent constructor runs first, and it calls this.init(), which resolves dynamically to the subclass override. But subclass fields are only installed after super() returns, so this.suffix is still undefined at that moment. Calling overridable methods from a base constructor is a genuine design hazard, not just a quiz trick.
`super(...)`- only in a derived constructor, and before any use of
this `super.m()`- starts the lookup one level up, keeps the current
this Implicit constructorconstructor(...args) { super(...args); }`new.target` in a base constructor- the most derived class that
newwas applied to `Object.getPrototypeOf(Sub)`Base, which is how statics are inherited`Symbol.species`- which constructor built-in methods use for their results
Depth guideline- more than two levels and the next lesson becomes relevant
Inheritance is a promise that the subclass will keep working when the base changes. Make that promise carefully.
Try it yourself
Inspect both chains
class A { static who() { return 'A'; } hello() { return 'a'; } }
class B extends A { }
class C extends B { hello() { return 'c/' + super.hello(); } }
const c = new C();
console.log(c.hello());
console.log(C.who(), Object.getPrototypeOf(C) === B, Object.getPrototypeOf(B) === A);
const chain = (v) => {
const out = [];
for (let p = Object.getPrototypeOf(v); p; p = Object.getPrototypeOf(p)) {
out.push(p.constructor?.name ?? '(none)');
}
return out.join(' -> ');
};
console.log('instance:', chain(c));
console.log('class: ', chain(C));
Add a third level, then print the instance chain and the class chain side by side. Where does Object.prototype appear in each?
A usable error hierarchy
class AppError extends Error {
constructor(message, options) {
super(message, options);
this.name = new.target.name;
}
}
class ValidationError extends AppError {
constructor(field, message) {
super(message ?? `${field} is invalid`);
this.field = field;
}
}
const err = new ValidationError('email');
console.log(String(err), err.field);
console.log(err instanceof ValidationError, err instanceof AppError, err instanceof Error);
console.log(JSON.stringify(err)); // message is non-enumerable, so it is missing
Add a ConflictError with status 409, then write a handle(err) function that narrows with instanceof and falls back for unknown errors.
Exercises
A two-level error hierarchy
Build AppError extends Error and ValidationError extends AppError. new AppError(message, options) forwards both arguments to super so { cause } keeps working, and sets name to the name of the class actually being constructed (so a subclass reports its own name without repeating itself). new ValidationError(field, message) calls up with the message, defaulting to "<field> is invalid", and stores field as an own property. Add a static AppError.is(value) that returns true only for instances of the class it is called on.
An abstract shape base class
Write a base class Shape that cannot be constructed directly: new Shape("x") throws a TypeError, while subclasses construct fine. It stores name, declares area() which throws an Error when a subclass has not overridden it, and provides describe() returning "<name>: <area>" with the area formatted to two decimals via toFixed(2). Then write Circle extends Shape taking a radius, and Square extends Shape taking a side, whose describe() appends " (a special rectangle)" by reusing the base implementation rather than rebuilding the string.
Check yourself
- What does this log?
- 'Bundefined' — Method lookup is dynamic, so the base constructor calls
B.prototype.label. Field initialisers onBonly run aftersuper()returns, sothis.suffixis stillundefinedwhenlabelreads it. Avoid calling overridable methods from a base constructor. - Why is
thisunavailable beforesuper()in a derived constructor? - The base constructor is what creates the object, so there is nothing to bind until it returns — In a derived class,
newdelegates object creation up the chain. The binding forthisstays uninitialised, like aconstin its TDZ, untilsuper()hands the object back down. Reading it early is a ReferenceError. - What does
class Sub extends Base {}link, apart from the prototypes of the instances? - It sets
Sub’s own prototype toBase, so static members are inherited through the class chain — There are two links:Sub.prototype -> Base.prototypefor instance members, andSub -> Basefor statics. Nothing is copied, which is why adding a static toBaselater is immediately visible onSub.Sub.prototype.constructorstill points atSub. - Which guard makes a base class abstract without blocking subclasses?
if (new.target === Shape) throw new TypeError()— When you writenew Circle(), the base constructor still runs butnew.targetisCircle, so the comparison only rejects direct construction ofShape.!new.targetcatches a call withoutnew, which classes already reject, and theinstanceofvariant is true for every subclass instance too.
Common mistakes
- Touching
thisbefore callingsuper()in a derived constructor. - Calling an overridable method from a base constructor, which runs before subclass fields exist.
- Assuming statics are copied. They are inherited through a second live link from the subclass to the base.
- Forgetting to set
nameon anErrorsubclass, so every log line says "Error". - Extending
Arrayand being caught out bynew Sub(3)meaning length 3, or bymapreturning the subclass. - Copying a method that uses
superonto another object.superis fixed to where the method was defined. - Building a four-level hierarchy when a function argument would have done.
Takeaways
extendslinks instance prototypes and the classes themselves, so both methods and statics are inherited.- In a derived constructor the object comes from the base, so
super()must run beforethis. super.method()starts the lookup one level up while keeping the current receiver.new.target === Baseis the idiomatic abstract-class guard;new this(...)keeps static factories subclass friendly.- Subclassing
Errorneeds aname, forwardedoptionsforcause, and nothing else on modern engines. - Subclass fields are installed after
super()returns, which makes base constructors calling overridable methods a hazard.