JAVASCRIPT / PROTOTYPES, CLASSES, AND INHERITANCE
Inheritance with extends and super
Build class hierarchies with extends, initialise them correctly with super() in constructors, and call the parent's version of a method with super.method().
What you will learn
- Wire up a subclass with extends and know it links both prototype and static chains
- Call super() before touching this in a derived constructor, and forward the right args
- Reuse an overridden method with super.method() instead of recursing through this
- Predict construction order: parent body, then child fields, then child body
Understanding Inheritance with extends and super
The line `class Square extends Rectangle` performs two separate wirings. It sets the prototype of `Square.prototype` to `Rectangle.prototype`, which is what makes instance method lookup fall through to the parent, and it sets `Square` itself to inherit from `Rectangle`, which is what makes static members inherited. `instanceof` is not a special class feature bolted on top: `sq instanceof Shape` is true only because walking that first chain upward from `sq` eventually arrives at `Shape.prototype`.
A derived constructor does not create its own object. `new Square(4)` runs Square's body, and `super(side, side)` delegates upward until a base constructor actually allocates the object, but it allocates using `new.target.prototype`, so the result is a Square rather than a Rectangle. Until `super()` returns, `this` is unbound inside the derived constructor, which is why touching `this` first throws a ReferenceError instead of quietly working. If a subclass adds no parameters of its own you can omit the constructor entirely; the implicit one is exactly `constructor(...args) { super(...args); }`.
Inside a method, `super` is resolved from where the method was written, not from `this`. Every method remembers a home object, `Square.prototype` for a method declared in Square, and `super.describe()` means: begin the lookup one link above that home object, then invoke what you find with the current `this`. That fixed starting point is why `this.describe()` inside `describe()` recurses forever while `super.describe()` climbs exactly one level, and it works the same in static methods, where the home object is the class object itself.
class Shape {
constructor(name) {
this.name = name;
}
area() {
return 0;
}
describe() {
return `${this.name} has area ${this.area()}`;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super('rectangle');
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(side) {
super(side, side);
this.name = 'square';
}
describe() {
return super.describe() + ' (all sides equal)';
}
}
const sq = new Square(4);
console.log(sq.describe());
console.log(sq instanceof Rectangle, sq instanceof Shape);
console.log(Object.getPrototypeOf(Square.prototype) === Rectangle.prototype);
console.log(Object.getPrototypeOf(Square) === Rectangle);extends creates the two prototype links and super is a fixed reference to the next link up, so a subclass can delegate both its construction and its method calls without ever naming its parent again.
Worked examples
this is unbound until super() returns
Shows that the order of super() and property assignment in a derived constructor is a hard rule, not a style preference.
class Base {
constructor() {
this.tag = 'base';
}
}
class Broken extends Base {
constructor() {
this.extra = 1;
super();
}
}
class Fixed extends Base {
constructor() {
super();
this.extra = 1;
}
}
try {
new Broken();
} catch (err) {
console.log(err instanceof ReferenceError);
}
console.log(JSON.stringify(new Fixed()));Example explained
Line 1`this.extra = 1` in Broken runs before any object exists, so reading `this` throws a ReferenceError at construction time.
Line 2The error is a ReferenceError, the same class of error as using a `let` binding before its declaration, because `this` really is uninitialised rather than wrong.
Line 3In Fixed, `super()` runs Base's body first, so `tag` is set before `extra`, which is the order JSON.stringify reports.
Line 4Only the base constructor creates the object; the derived constructor just adds to whatever super() handed back.
Static inheritance and super in a static method
Demonstrates the second link extends creates: the subclass object itself inherits from the parent class object.
class Registry {
static items = [];
static register(item) {
this.items = [...this.items, item];
return `${this.name}: ${this.items.length}`;
}
}
class Plugins extends Registry {
static items = [];
static register(item) {
return super.register(item.toUpperCase());
}
}
console.log(Plugins.register('cache'));
console.log(JSON.stringify(Plugins.items), JSON.stringify(Registry.items));
console.log(Object.getPrototypeOf(Plugins) === Registry);Example explained
Line 1`super.register(...)` in a static method starts its lookup on `Object.getPrototypeOf(Plugins)`, which extends set to `Registry`.
Line 2Registry.register runs with `this` still equal to `Plugins`, so `this.name` is 'Plugins' and `this.items` is Plugins' own static field.
Line 3Registry.items stays empty, proving the inherited method wrote through `this` rather than to the class it was defined on.
Line 4Without extends, `Plugins.register` would not exist at all unless copied over by hand.
A subclass with no constructor at all
Shows the implicit derived constructor forwarding arguments, and that the parent constructor still sees the subclass prototype.
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = this.constructor.name;
this.field = field;
}
}
class RequiredFieldError extends ValidationError {}
const err = new RequiredFieldError('email', 'email is required');
console.log(err.name);
console.log(err.message, '/', err.field);
console.log(err instanceof RequiredFieldError, err instanceof Error);
console.log(String(err));Example explained
Line 1RequiredFieldError declares no constructor, so it gets `constructor(...args) { super(...args); }` and both arguments reach ValidationError unchanged.
Line 2`this.constructor.name` is 'RequiredFieldError' because the object was allocated with the constructor you called `new` on, even though the code setting it lives in the parent.
Line 3`super(message)` hands the message to Error's constructor, which is the only place that can initialise it properly.
Line 4`String(err)` uses Error.prototype.toString, inherited two links up, and it reads the `name` the subclass instance ended up with.
Important notes
Class field initialisers in a derived class run immediately after `super()` returns, so if the parent constructor calls a method the child overrides, that override sees the child's fields as undefined.
`super` is fixed at definition time to the method's home object, so pulling a method with `super` in it out of the class and attaching it to a plain object breaks the link; arrow functions written inside a method do keep that method's `super`.
Common mistakes
Assigning `this.x = value` before calling `super()` in a derived constructor: every single construction throws a ReferenceError, so the class is unusable rather than subtly wrong.
Writing a constructor in the subclass and forgetting `super()`, or calling it with no arguments: you get a ReferenceError when the constructor returns, or the parent's properties silently end up undefined because nothing was forwarded.
Writing `this.describe()` inside the overriding `describe()` to reach the parent version: lookup starts at the instance again, finds the override, and recurses until RangeError: Maximum call stack size exceeded.
Try it yourself
Change, predict, then run
Write `class Account { constructor(owner, balance) }` with a `summary()` method returning owner and balance, then `class SavingsAccount extends Account` taking `(owner, balance, rate)` whose `summary()` returns `super.summary()` plus the yearly interest. Log the summary of `new SavingsAccount('Ada', 1000, 0.05)` and check that it is `instanceof Account`.
Open the JavaScript workspaceCheck your understanding
A base class constructor ends by calling `this.report()`. A subclass overrides `report()`, and that override reads a field the subclass declares as `size = 10`. What happens when you construct the subclass?
- The subclass's report() runs, but size is undefined, because subclass field initialisers only run after super() returns
- The base class's report() runs, because inside the base constructor `this` is still a plain base instance
- A ReferenceError, because subclass fields cannot be reached until the subclass constructor body starts
- The subclass's report() runs and size is 10, because field declarations are installed before any constructor body
Show answer
Method lookup always goes through the object's own prototype, and `new` recorded the subclass as new.target, so the object already has the subclass prototype while the base constructor is still running and the override wins. Option 3 is tempting because a field declaration looks like part of the class shape, but in a derived class those declarations are assignments injected immediately after `super()` returns, so at that earlier moment `size` does not exist yet and reading it yields undefined rather than throwing.