5 inheritance WITHOUT class
GOAL
Make Mage extend Player armed with nothing but constructor functions, Object.create, and .call — the raw parts that `class extends` is compiled into. Do it by hand once and `super` / `extends` turn from magic into shorthand.
CONCEPT
`class extends` bundles three jobs that are really independent: 1. state: Base.call(this, ...) replays the base constructor on the freshly created object ("super(...)"), 2. behavior: Child.prototype = Object.create(Base.prototype) so method lookup falls through ("extends"), 3. identity: put Child.prototype.constructor back, because step 2 discarded the object that carried it.
HINT
Without `super`, "run the base method on this instance" is spelled Base.prototype.method.call(this).
MIRRORS
Every pre-ES6 engine and UI toolkit shipped an extend()/inherits() helper that did exactly these three steps to build its type ladder (Mage → Player → Entity). When you step into a third-party library's internals in the debugger, this is the wiring you land in.
Run
node 05-inheritance-without-class.cjsSource
'use strict';
// ---------------------------------------------------------------------
// PROTO-DOJO · Module 1 / Exercise 5 — inheritance WITHOUT `class`
//
// GOAL: Make Mage extend Player armed with nothing but constructor
// functions, Object.create, and .call — the raw parts that
// `class extends` is compiled into. Do it by hand once and
// `super` / `extends` turn from magic into shorthand.
// CONCEPT: `class extends` bundles three jobs that are really
// independent: 1. state: Base.call(this, ...) replays the
// base constructor on the freshly created object
// ("super(...)"), 2. behavior: Child.prototype =
// Object.create(Base.prototype) so method lookup falls
// through ("extends"), 3. identity: put
// Child.prototype.constructor back, because step 2 discarded
// the object that carried it.
// HINT: Without `super`, "run the base method on this instance" is
// spelled Base.prototype.method.call(this).
// MIRRORS: Every pre-ES6 engine and UI toolkit shipped an
// extend()/inherits() helper that did exactly these three
// steps to build its type ladder (Mage → Player → Entity).
// When you step into a third-party library's internals in the
// debugger, this is the wiring you land in.
//
// Run: node 05-inheritance-without-class.cjs
// ---------------------------------------------------------------------
const assert = require('node:assert');
function Player(id, hits) {
this.id = id;
this.hits = hits;
this.alive = true;
}
Player.prototype.hitCount = function () {
return this.hits.length;
};
Player.prototype.render = function () {
return '<sprite class="player ' + this.id + '">' + this.hitCount() + ' hits</sprite>';
};
function Mage(id, hits) {
// -- TODO 1 -- base state (id, hits, alive) is never set up on the new mage.
// Borrow the base constructor: run Player's body against THIS new object
// (no `super(...)` exists outside class syntax).
}
// -- TODO 2 -- wire the chain: mage instances must reach hitCount and render
// through Player.prototype. Rules: no `class`; and NOT
// `Mage.prototype = Player.prototype` — the render override below would
// then
// stomp on every plain knight in the roster. You need one fresh intermediate
// object whose hidden link is Player.prototype.
// (insert the wiring HERE, before the render override below)
// -- TODO 3 -- TODO 2's rewiring discarded the default `{ constructor: Mage }`
// object — repair the constructor property.
// (Bonus: make it non-enumerable, like the original.)
// (insert the repair HERE)
Mage.prototype.render = function () {
// -- TODO 4 -- call the BASE render ON THIS INSTANCE and wrap its
// output in <aura>…</aura>. There is no `super` here — go through
// Player.prototype.
const baseOutput = ''; // ← replace: base render, this instance
return '<aura>' + baseOutput + '</aura>';
};
// -- checks -----------------------------------------------------------
const mage = new Mage('mage', [
{ at: 120, damage: 8 },
{ at: 340, damage: 11 },
{ at: 610, damage: 5 },
]);
assert.ok(mage instanceof Mage, 'sanity — should never fail');
assert.ok(
mage instanceof Player,
'TODO 2: instanceof walks the hidden chain — Mage.prototype must link to Player.prototype'
);
assert.strictEqual(
Object.getPrototypeOf(Mage.prototype),
Player.prototype,
'TODO 2: exactly one fresh intermediate object, chained with Object.create — not a shared or copied one'
);
assert.strictEqual(mage.id, 'mage',
'TODO 1: the base constructor never ran against the new mage — .call it with this');
assert.strictEqual(mage.alive, true, 'TODO 1: base init should have set alive');
assert.strictEqual(mage.hitCount(), 3, 'hitCount must be INHERITED through the chain, not redefined');
assert.strictEqual(
mage.render(),
'<aura><sprite class="player mage">3 hits</sprite></aura>',
'TODO 4: Player.prototype.render.call(this), then wrap the result'
);
assert.strictEqual(
mage.constructor,
Mage,
'TODO 3: after replacing the prototype object, .constructor falls through the chain to Player — repair it'
);
// the base class must remain untouched by all of the above:
const knight = new Player('knight', [{ at: 90, damage: 14 }, { at: 400, damage: 6 }]);
assert.strictEqual(knight.render(), '<sprite class="player knight">2 hits</sprite>',
'a plain Player must NOT get the aura wrapper — did TODO 2 assign Player.prototype directly?');
assert.strictEqual(Player.prototype.constructor, Player, 'base identity intact');
console.log('PASS — 05-inheritance-without-class: state, behavior chain, and identity wired by hand');