3 override and super by hand
GOAL
Build one subclass two ways: first with `class ... extends` + `super`, then desugared by hand — and prove that `super.render()` is nothing but `Base.prototype.render.call(this)` sitting on top of a prototype link.
CONCEPT
`class Sub extends Base` wires TWO chains at once: 1. Sub.prototype --[[Prototype]]--> Base.prototype (instance methods) 2. Sub --[[Prototype]]--> Base (statics!) `super.m()` means: look `m` up starting ABOVE the prototype I was defined on, then invoke it with MY `this`. Desugared: `Base.prototype.m.call(this)`. And once you write `Sub.prototype = Object.create(Base.prototype)`, the `.constructor` back-link is on YOU to repair — `extends` does it silently.
HINT
- Base constructor by hand: `Player.call(this, name)` (that IS `super(name)`). - Chain by hand: `Object.create(Player.prototype)`, then fix `.constructor`. - Statics by hand: `Object.setPrototypeOf(MageDesugared, Player)`.
MIRRORS
Every game engine and UI toolkit ships a type ladder where a subtype overrides a lifecycle method and calls up the chain first — `class Mage extends Player { render() { super.render(); ... } }` — and before class syntax existed, those same libraries wired the ladder with an `extend(Base, {...})` helper that did exactly the desugared form below.
Run
node 03-override-and-super-by-hand.cjsSource
// ---------------------------------------------------------------------
// 03-classes-desugared / 03-override-and-super-by-hand.cjs
//
// GOAL: Build one subclass two ways: first with `class ... extends` +
// `super`, then desugared by hand — and prove that
// `super.render()` is nothing but
// `Base.prototype.render.call(this)` sitting on top of a
// prototype link.
//
// CONCEPT: `class Sub extends Base` wires TWO chains at once: 1.
// Sub.prototype --[[Prototype]]--> Base.prototype
// (instance methods) 2. Sub --[[Prototype]]-->
// Base (statics!) `super.m()` means: look `m` up
// starting ABOVE the prototype I was defined on, then invoke
// it with MY `this`. Desugared: `Base.prototype.m.call(this)`.
// And once you write `Sub.prototype =
// Object.create(Base.prototype)`, the `.constructor` back-link
// is on YOU to repair — `extends` does it silently.
//
// HINT: - Base constructor by hand: `Player.call(this, name)` (that
// IS `super(name)`).
// - Chain by hand: `Object.create(Player.prototype)`, then fix
// `.constructor`.
// - Statics by hand: `Object.setPrototypeOf(MageDesugared,
// Player)`.
//
// MIRRORS: Every game engine and UI toolkit ships a type ladder where a
// subtype overrides a lifecycle method and calls up the chain
// first — `class Mage extends Player { render() {
// super.render(); ... } }` — and before class syntax existed,
// those same libraries wired the ladder with an `extend(Base,
// {...})` helper that did exactly the desugared form below.
//
// Run: node 03-override-and-super-by-hand.cjs
// ---------------------------------------------------------------------
'use strict';
const assert = require('node:assert');
// -- Base type (do not edit) ------------------------------------------
// Deliberately PRE-CLASS style, like the older releases of the third-party
// `engine` library. This matters: a real `class` constructor refuses a plain
// [[Call]], so `Player.call(this)` would throw if Player were a class —
// hand-desugared subclassing only works against function-style bases (or via
// Reflect.construct). A `class` CAN, however, extend a function-style base
// without complaint — try it below.
function Player(name) {
this.name = name;
this.rendered = false;
}
Player.prototype.render = function render() {
this.rendered = true;
return ['drawSprite', 'drawHealthBar'];
};
Player.defaultColor = '#2caffe';
// -- TODO 1: the sugared subclass -------------------------------------
// Make MageSugared extend Player so that:
// - the constructor takes (name, auraRadius), passes name up via super(name),
// then sets this.auraRadius
// - render() overrides the base: run the BASE steps first (super), then
// append 'drawAura' to the returned array
class MageSugared {
// ← replace: extends, constructor(name, auraRadius), render() override
}
// -- TODO 2: the same subclass, desugared -----------------------------
function MageDesugared(name, auraRadius) {
// TODO 2a: call the base constructor on `this` (this IS what super(name) does)
this.auraRadius = auraRadius;
}
// TODO 2b: wire the INSTANCE chain here — MageDesugared.prototype must
// delegate to Player.prototype, and `.constructor` must be repaired.
// (Do it BEFORE the render assignment below, or you'll wipe it out.)
// TODO 2c: wire the STATIC chain — MageDesugared.defaultColor should be
// found via delegation to Player (class `extends` links constructors too).
MageDesugared.prototype.render = function render() {
// TODO 2d: hand-rolled super call — run the base render with MY
// `this`, then append 'drawAura'. No `super` keyword allowed out
// here.
const baseSteps = []; // ← replace: base render, this instance
return [...baseSteps, 'drawAura'];
};
// -- Checks (do not edit) ---------------------------------------------
function verifyMage(Ctor, label) {
const m = new Ctor('mage', 2);
assert.ok(m instanceof Ctor, `${label}: instanceof own constructor`);
assert.ok(m instanceof Player,
`${label}: instanceof Player — the subclass prototype must DELEGATE to ` +
`Player.prototype (extends / Object.create in 2b, never a copy)`);
assert.strictEqual(m.name, 'mage',
`${label}: the base constructor must run against the new instance — ` +
`that is all super(name) is (2a): Player.call(this, name)`);
assert.strictEqual(m.auraRadius, 2,
`${label}: write a constructor(name, auraRadius) that first hands ` +
`name to the base (super(name) / Player.call(this, name)), then sets ` +
`this.auraRadius`);
const steps = m.render();
assert.deepStrictEqual(steps, ['drawSprite', 'drawHealthBar', 'drawAura'],
`${label}: the override must DELEGATE to the base render first ` +
`(super.render() === Player.prototype.render.call(this), 2d), then extend it`);
assert.strictEqual(m.rendered, true,
`${label}: proof the base ran with YOUR \`this\` — it flipped this.rendered`);
assert.ok(!Object.prototype.hasOwnProperty.call(m, 'render'),
`${label}: the override lives on the subclass PROTOTYPE, shadowing the base ` +
`by coming first in the lookup chain`);
assert.strictEqual(Ctor.prototype.constructor, Ctor,
`${label}: after Sub.prototype = Object.create(Base.prototype) you must ` +
`repair .constructor yourself (2b) — \`extends\` does it silently`);
assert.strictEqual(Object.getPrototypeOf(Ctor.prototype), Player.prototype,
`${label}: chain #1 — Sub.prototype delegates to Base.prototype (2b)`);
assert.strictEqual(Ctor.defaultColor, '#2caffe',
`${label}: chain #2 — statics are inherited because the CONSTRUCTOR ` +
`delegates to the base constructor (2c: Object.setPrototypeOf(Sub, Base))`);
}
verifyMage(MageSugared, 'MageSugared (TODO 1)');
verifyMage(MageDesugared, 'MageDesugared (TODO 2)');
// One last identity check: both overrides shadow, neither destroyed the
// base.
assert.deepStrictEqual(new Player('knight').render(), ['drawSprite', 'drawHealthBar'],
'overriding on a subclass prototype must never mutate the base prototype');
console.log('OK — 03-override-and-super-by-hand: super is a .call(this) wearing plate armor.');