4 live reclass setprototypeof
GOAL
Swap the CLASS of a live object without swapping the object: implement `updatePlayerType(player, 'warrior')` so the very same instance — still held by game.players[], the camera, the hud — starts behaving as a Warrior while keeping every bit of its per-instance state.
CONCEPT
Both `instanceof` and method lookup follow the instance's [[Prototype]] link, and that link can be rewritten: `Object.setPrototypeOf(obj, OtherCtor.prototype)`. Own properties (hits, selected, settings) do not go anywhere — they sit on the object itself. Only the delegation target moves. That is how an object can "re-class" halfway through a match while every outside reference to it keeps working. (Cost note: engines de-optimise objects whose prototype changes — fine for an occasional player-triggered class change, wrong inside a hot loop.)
HINT
Find the new prototype in the registry, swap it in with Object.setPrototypeOf, throw for class names the registry has never heard of (the `engine` does the same, "Error 17"), and return the SAME reference.
MIRRORS
Entity systems and widget libraries pull this exact trick: when a player picks a new class, or a control is re-typed at runtime, the library does if (Object.setPrototypeOf) { Object.setPrototypeOf(entity, registry[newType].prototype); } rather than allocate a replacement — the scene graph, input handlers and hud all keep pointing at the object they already know.
Run
node 04-live-reclass-setprototypeof.cjsSource
// ---------------------------------------------------------------------
// 03-classes-desugared / 04-live-reclass-setprototypeof.cjs
//
// GOAL: Swap the CLASS of a live object without swapping the object:
// implement `updatePlayerType(player, 'warrior')` so the very
// same instance — still held by game.players[], the camera, the
// hud — starts behaving as a Warrior while keeping every bit of
// its per-instance state.
//
// CONCEPT: Both `instanceof` and method lookup follow the instance's
// [[Prototype]] link, and that link can be rewritten:
// `Object.setPrototypeOf(obj, OtherCtor.prototype)`. Own
// properties (hits, selected, settings) do not go anywhere —
// they sit on the object itself. Only the delegation target
// moves. That is how an object can "re-class" halfway through
// a match while every outside reference to it keeps working.
// (Cost note: engines de-optimise objects whose prototype
// changes — fine for an occasional player-triggered class
// change, wrong inside a hot loop.)
//
// HINT: Find the new prototype in the registry, swap it in with
// Object.setPrototypeOf, throw for class names the registry has
// never heard of (the `engine` does the same, "Error 17"), and
// return the SAME reference.
//
// MIRRORS: Entity systems and widget libraries pull this exact trick:
// when a player picks a new class, or a control is re-typed at
// runtime, the library does if (Object.setPrototypeOf) {
// Object.setPrototypeOf(entity, registry[newType].prototype);
// } rather than allocate a replacement — the scene graph,
// input handlers and hud all keep pointing at the object they
// already know.
//
// Run: node 04-live-reclass-setprototypeof.cjs
// ---------------------------------------------------------------------
'use strict';
const assert = require('node:assert');
// -- Setup: a mini class registry, engine-style (do not edit) ---------
const playerTypes = {};
class Player {
constructor(game, settings) {
this.game = game;
this.settings = settings;
this.name = settings.name;
this.hits = settings.hits.slice();
this.selected = false;
}
select() { this.selected = true; }
}
class Archer extends Player {
render() { return `arrows[${this.hits.join('->')}]`; }
}
class Warrior extends Player {
render() { return `swings[${this.hits.join('|')}]`; }
shieldBash() { return true; } // warrior-only API, must appear after the switch
}
playerTypes.archer = Archer;
playerTypes.warrior = Warrior;
// -- TODO 1: implement the class switch -------------------------------
// Rules (all enforced below):
// 1. Unknown class name → throw an Error whose message contains 'unknown'
// (mirror of the engine's "Error 17: requested player class does not exist").
// 2. The SAME object must come back — never `new` a replacement; the
// game's
// players array (and every camera/hud back-reference) holds this ref.
// 3. After the call the instance must delegate to the NEW class's prototype.
// 4. All own state (hits, selected, settings, game) must survive untouched.
function updatePlayerType(player, newTypeName) {
// ...your code here (it is 3-5 lines)...
}
// -- Checks (do not edit) ---------------------------------------------
const game = { players: [] };
const p = new Archer(game, { name: 'knight', hits: [12, 7, 30] });
game.players.push(p);
p.select(); // per-instance state to preserve
assert.strictEqual(p.render(), 'arrows[12->7->30]', 'sanity: starts as an archer');
assert.ok(p instanceof Archer, 'sanity: starts as Archer');
const returned = updatePlayerType(p, 'warrior');
assert.ok(returned instanceof Warrior,
'TODO: after the switch the object should BE a Warrior — do not build a new ' +
'object, swap the [[Prototype]] of the existing one (Object.setPrototypeOf)');
assert.strictEqual(returned, p,
'TODO: must return the SAME reference — game.players[0], the camera and the ' +
'hud all point at this exact object');
assert.strictEqual(game.players[0], p,
'TODO: the game never noticed: its array still holds the original reference');
assert.ok(!(p instanceof Archer),
'TODO: Archer.prototype is no longer in the chain — instanceof is a LIVE walk ' +
'of [[Prototype]] links, not a birth certificate');
assert.ok(p instanceof Player,
'TODO: still a Player: Warrior.prototype itself delegates to Player.prototype');
assert.strictEqual(p.render(), 'swings[12|7|30]',
'TODO: method lookup now resolves through Warrior.prototype — same hits, ' +
'new behavior');
assert.strictEqual(typeof p.shieldBash, 'function',
'TODO: warrior-only API appears: lookup walks the NEW chain');
assert.deepStrictEqual(p.hits, [12, 7, 30],
'TODO: own state untouched — setPrototypeOf changes the delegation target, ' +
'never the own properties');
assert.strictEqual(p.selected, true,
'TODO: selection survived the class switch (the engine keeps own props on re-class)');
assert.strictEqual(p.name, 'knight', 'TODO: name survived');
assert.throws(() => updatePlayerType(p, 'necromancer'), /unknown/i,
'TODO: unknown class must throw (engine Error 17: "requested player class does ' +
'not exist") — silently keeping the old prototype hides real bugs');
console.log('OK — 04-live-reclass-setprototypeof: one object, new class, zero broken references.');