js-dojo

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

Source

'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');

Solution