1 desugar the suit
GOAL
Convince yourself that `class` is nothing but prototypes in formal wear: rewrite `PlayerClass` by hand as `PlayerDesugared` (a plain constructor function plus some prototype surgery) until ONE shared contract-check passes for both of them.
CONCEPT
A class declaration is a bundle of separately-writable parts: a constructor function, methods installed as NON-ENUMERABLE properties of Ctor.prototype, accessors as getters on that same prototype, statics as own properties of the constructor, an automatic `prototype.constructor` back-link, and a "refuse to run without new" guard. None of it is magic — game engines and UI toolkits shipped for years in precisely this hand-rolled shape (`var Player = function () {...}; extend(Player.prototype, {...})`).
HINT
- Method & getter: `Object.defineProperties(PlayerDesugared.prototype, {...})`. defineProperty defaults to `enumerable: false`; plain assignment (`proto.m = fn`) does NOT — it creates enumerable properties. - The "called without new" guard: test `new.target === undefined`. - Statics are just properties hung on the function object itself.
MIRRORS
Open the source of any engine or widget library that predates ES2015: `var Entity = function () {...}; extend(Entity.prototype, { spawn, update, render, ... })`. The `class` you write today is that same machine with a nicer coat on.
Run
node 01-desugar-the-suit.cjsSource
// ---------------------------------------------------------------------
// 03-classes-desugared / 01-desugar-the-suit.cjs
//
// GOAL: Convince yourself that `class` is nothing but prototypes in
// formal wear: rewrite `PlayerClass` by hand as `PlayerDesugared`
// (a plain constructor function plus some prototype surgery)
// until ONE shared contract-check passes for both of them.
//
// CONCEPT: A class declaration is a bundle of separately-writable
// parts: a constructor function, methods installed as
// NON-ENUMERABLE properties of Ctor.prototype, accessors as
// getters on that same prototype, statics as own properties of
// the constructor, an automatic `prototype.constructor`
// back-link, and a "refuse to run without new" guard. None of
// it is magic — game engines and UI toolkits shipped for years
// in precisely this hand-rolled shape (`var Player = function
// () {...}; extend(Player.prototype, {...})`).
//
// HINT: - Method & getter:
// `Object.defineProperties(PlayerDesugared.prototype, {...})`.
// defineProperty defaults to `enumerable: false`; plain
// assignment (`proto.m = fn`) does NOT — it creates enumerable
// properties.
// - The "called without new" guard: test `new.target ===
// undefined`.
// - Statics are just properties hung on the function object
// itself.
//
// MIRRORS: Open the source of any engine or widget library that
// predates ES2015: `var Entity = function () {...};
// extend(Entity.prototype, { spawn, update, render, ... })`.
// The `class` you write today is that same machine with a
// nicer coat on.
//
// Run: node 01-desugar-the-suit.cjs
// ---------------------------------------------------------------------
'use strict';
const assert = require('node:assert');
// -- Reference implementation (do not edit) ---------------------------
class PlayerClass {
constructor(name, hits) {
this.name = name;
this.hits = hits.slice();
}
loadHits(hits) {
this.hits = hits.slice();
return this;
}
get hitCount() {
return this.hits.length;
}
static fromEvents(name, events) {
return new this(name, events.map((e) => e.damage));
}
}
// -- YOUR DESUGARED VERSION -------------------------------------------
// TODO 1: make the constructor copy `name` and `hits` onto the instance,
// and throw a TypeError when called WITHOUT `new` (hint: new.target).
function PlayerDesugared(name, hits) {
// ...
}
// TODO 2: install `loadHits` on PlayerDesugared.prototype as a NON-ENUMERABLE
// method (class methods never show up in Object.keys / for-in).
// TODO 3: install the `hitCount` GETTER on the prototype (also
// non-enumerable).
// TODO 4: install the static `fromEvents` on the constructor function itself.
// (Note the reference class says `new this(...)` — keep that spirit.)
// -- Shared contract check (do not edit) ------------------------------
function verifyPlayerContract(Ctor, label) {
const p = new Ctor('knight', [12, 7, 30]);
assert.strictEqual(p.name, 'knight',
`${label}: TODO 1 — the constructor must copy \`name\` onto the instance`);
assert.deepStrictEqual(p.hits, [12, 7, 30],
`${label}: TODO 1 — the constructor must copy \`hits\` onto the instance`);
assert.ok(p instanceof Ctor,
`${label}: instances must satisfy instanceof`);
assert.strictEqual(Object.getPrototypeOf(p), Ctor.prototype,
`${label}: instanceof works because the instance's [[Prototype]] IS Ctor.prototype`);
assert.strictEqual(typeof p.loadHits, 'function',
`${label}: TODO 2 — loadHits should be reachable through the prototype chain`);
assert.ok(!Object.prototype.hasOwnProperty.call(p, 'loadHits'),
`${label}: TODO 2 — loadHits must live on the PROTOTYPE, not on each instance`);
const q = new Ctor('rogue', []);
assert.strictEqual(p.loadHits, q.loadHits,
`${label}: TODO 2 — one function object shared by every instance`);
p.loadHits([5, 9]);
assert.deepStrictEqual(p.hits, [5, 9],
`${label}: TODO 2 — loadHits must operate on \`this\``);
assert.strictEqual(p.hitCount, 2,
`${label}: TODO 3 — hitCount is a getter derived from this.hits`);
assert.ok(!Object.prototype.hasOwnProperty.call(p, 'hitCount'),
`${label}: TODO 3 — the getter lives on the prototype, not the instance`);
assert.strictEqual(Ctor.prototype.constructor, Ctor,
`${label}: prototype.constructor must point back at the constructor ` +
`(if you replaced the whole prototype object, you broke this back-link)`);
assert.deepStrictEqual(Object.keys(Ctor.prototype), [],
`${label}: class methods are NON-ENUMERABLE — plain assignment ` +
`(proto.m = fn) makes them enumerable; use Object.defineProperty`);
assert.throws(() => Ctor('mage', []), TypeError,
`${label}: TODO 1 — calling without \`new\` must throw a TypeError, ` +
`like class constructors do (check new.target)`);
assert.strictEqual(typeof Ctor.fromEvents, 'function',
`${label}: TODO 4 — statics are plain properties of the constructor function`);
const f = Ctor.fromEvents('paladin', [{ at: 40, damage: 18 }, { at: 55, damage: 25 }]);
assert.ok(f instanceof Ctor,
`${label}: TODO 4 — fromEvents should construct via \`new this(...)\``);
assert.deepStrictEqual(f.hits, [18, 25],
`${label}: TODO 4 — fromEvents maps incoming events to their damage`);
}
verifyPlayerContract(PlayerClass, 'PlayerClass (reference)');
verifyPlayerContract(PlayerDesugared, 'PlayerDesugared (yours)');
console.log('OK — 01-desugar-the-suit: the suit came off and nothing changed.');