Inheritance: extends and super
Building a new class on an existing one. Powerful, frequently overused, and the source of most bad OOP code ever written. You will learn both halves of that sentence.
By the end: Extend a class correctly, including its constructor.
A luxury coach and a danfo share a great deal: a plate number, an owner, a place in the fleet list. A luxury coach adds air conditioning and reclining seats. Rather than writing the shared parts twice, you build the specific thing on top of the general one.
class Bus {
constructor(plate, owner) {
this.plate = plate;
this.owner = owner;
}
describe() {
return `${this.plate}, owned by ${this.owner}`;
}
}
class LuxuryBus extends Bus {
constructor(plate, owner, seats) {
super(plate, owner); // build the Bus part first
this.seats = seats;
}
refit(n) {
this.seats = n;
return this;
}
}
const lux = new LuxuryBus('LAG-441-XA', 'C. Okafor', 49);
console.log(lux.describe()); // inherited from Bus
console.log(lux.refit(45).seats);
console.log('own keys:', Object.keys(lux));
console.log('instanceof LuxuryBus:', lux instanceof LuxuryBus);
console.log('instanceof Bus:', lux instanceof Bus);
Output
What extends actually did
It linked two chains, one for instances and one for the classes themselves. The same idea from lesson 06, applied twice.
class Bus { describe() { return 'a bus'; } }
class LuxuryBus extends Bus {}
const lux = new LuxuryBus();
// Instance chain: lux -> LuxuryBus.prototype -> Bus.prototype -> Object.prototype
console.log(Object.getPrototypeOf(lux) === LuxuryBus.prototype);
console.log(Object.getPrototypeOf(LuxuryBus.prototype) === Bus.prototype);
console.log(Object.getPrototypeOf(Bus.prototype) === Object.prototype);
// Class chain, which is how static members are inherited.
console.log(Object.getPrototypeOf(LuxuryBus) === Bus);
// So describe is found two steps up.
console.log(lux.describe());
console.log('LuxuryBus.prototype owns describe?', Object.hasOwn(LuxuryBus.prototype, 'describe'));
Output
super, and the rule about calling it
class Bus {
constructor(plate) { this.plate = plate; }
}
class TooEager extends Bus {
constructor(plate) {
try {
this.seats = 49; // before super: this does not exist yet
super(plate);
} catch (err) {
console.log('1:', err.name + ':', err.message);
super(plate);
}
}
}
new TooEager('a');
class Forgetful extends Bus {
constructor(plate) {
// no super() call at all
}
}
try {
new Forgetful('b');
} catch (err) {
console.log('2:', err.name + ':', err.message);
}
Output
class Bus {
constructor(plate, owner) {
this.plate = plate;
this.owner = owner;
}
describe() { return `${this.plate} owned by ${this.owner}`; }
}
// No constructor here. JavaScript inserts constructor(...args) { super(...args); }
class Danfo extends Bus {
get kind() { return 'danfo'; }
}
const d = new Danfo('ABJ-772-KM', 'unknown');
console.log(d.describe(), '|', d.kind);
Output
Overriding, and calling the original
Define a method with the same name and the subclass version wins, because lookup stops at the first match. super.method() reaches past your override to the parent's version.
class Bus {
constructor(plate) { this.plate = plate; }
describe() {
return `${this.plate}`;
}
prepare() {
return ['check tyres', 'check oil'];
}
}
class LuxuryBus extends Bus {
constructor(plate, extra) {
super(plate);
this.extra = extra;
}
// Replaces the parent version.
describe() {
return `${super.describe()}, fitted with ${this.extra}`;
}
// Adds to the parent's list instead of rewriting it.
prepare() {
return [...super.prepare(), 'test the air conditioning', 'clean the seat covers'];
}
}
const lux = new LuxuryBus('LAG-441-XA', 'air conditioning');
console.log(lux.describe());
console.log(lux.prepare());
Output
class Base {
greet() { return 'base greeting'; }
}
class Child extends Base {
arrowWorks() {
return [1].map(() => super.greet()); // fine
}
methodWorks() {
const parent = super.greet(); // capture it, then use it
return [1].map(function () { return parent; });
}
}
const c = new Child();
console.log(c.arrowWorks());
console.log(c.methodWorks());
Output
Three levels, and why that is usually too many
class Vehicle {
constructor(plate) { this.plate = plate; }
describe() { return this.plate; }
}
class Bus extends Vehicle {
describe() { return `${super.describe()} [bus]`; }
}
class LuxuryBus extends Bus {
describe() { return `${super.describe()} [luxury]`; }
}
class SleeperBus extends LuxuryBus {
describe() { return `${super.describe()} [sleeper]`; }
}
console.log(new SleeperBus('LAG-441-XA').describe());
// To know what one method does you now have to read four files.
let link = Object.getPrototypeOf(new SleeperBus('x'));
const chain = [];
while (link && link.constructor !== Object) {
chain.push(link.constructor.name);
link = Object.getPrototypeOf(link);
}
console.log('chain:', chain.join(' -> '));
Output
Extending built-ins
class Fleet extends Array {
get plates() { return this.map(b => b.plate); }
ready() { return this.filter(b => b.ready); }
}
const fleet = new Fleet();
fleet.push({ plate: 'LAG-441-XA', ready: true });
fleet.push({ plate: 'ABJ-772-KM', ready: false });
console.log(fleet.plates);
console.log(fleet.ready());
console.log('still a real array:', Array.isArray(fleet), '| length:', fleet.length);
// Custom errors are the most useful built-in to extend.
class FullyBookedError extends Error {
constructor(route) {
super(`${route} is fully booked.`);
this.name = 'FullyBookedError';
this.route = route;
}
}
try {
throw new FullyBookedError('Lagos - Abuja');
} catch (err) {
console.log(err.name + ':', err.message);
console.log('catchable by type:', err instanceof FullyBookedError, err instanceof Error);
console.log('carries data:', err.route);
}
Output
The test for whether inheritance is the right tool
class Bus {
constructor(plate, seats) {
this.plate = plate;
this.seats = seats;
}
refit(n) { this.seats = n; return this; }
}
// A danfo has fixed benches, not numbered seats. So this override lies.
class Danfo extends Bus {
constructor(plate) { super(plate, 0); }
refit() {
throw new Error('A danfo has benches; it cannot be refitted with numbered seats.');
}
}
function prepareAll(buses) {
for (const b of buses) {
try {
b.refit(49);
console.log('refitted', b.plate);
} catch (err) {
console.log('broke on', b.plate + ':', err.message);
}
}
}
prepareAll([new Bus('LAG-441-XA', 14), new Danfo('KAN-905-BD')]);
Output
prepareAll was written against Bus and is now wrong for reasons it cannot see. The honest fix is that Danfo is not a kind of Bus as Bus is currently defined. Either narrow Bus to the things all buses truly share, or use composition. Lesson 14 does both.
Your turn: extend it properly
Departure is given. Write NightBus extends Departure with three changes:
- an extra
escortproperty; - a
describe()that callssuper.describe()and adds the escort on the end; - a
checklist()that adds two items to the parent's list without rewriting it.
One rule: NightBus must still work anywhere Departure works.
Output
Recap
extendslinks two prototype chains: one for instances, one for the classes, which is how statics are inherited too.- In a subclass constructor, call
super()before touchingthis, and call it at all. - An override wins because lookup stops at the first match;
super.method()reaches the parent's version so you can extend rather than replace. - Keep chains to about two levels. Every level is another file a reader must hold in mind.
- Extending
Erroris one of the most useful things inheritance does; a subclass that must throw from an inherited method is a sign inheritance was wrong.