6 getPlayerById over a [[Prototype]] chain
GOAL
Capstone: write a roster lookup that resolves ids the way the language itself resolves properties — closest level first, follow the hidden link upward, stop at the first match, hand back null when there is nothing left to climb — by walking the chain by hand with Object.getPrototypeOf.
CONCEPT
A read like `scope.players` only ever lands on the CLOSEST `players` array (shadowing!). Arrays never merge across levels — to search every roster you have to iterate: current object → Object.getPrototypeOf → … → null, and at each stop inspect that level's OWN array (Object.hasOwn) so no roster is scanned twice.
HINT
for (let o = scope; o !== null; o = Object.getPrototypeOf(o)) { … }
MIRRORS
Registries that cascade in layers — a match's own roster, the party the match belongs to, the campaign that owns every party; plugin hooks that fall back to app-level then framework-level handlers; DI containers with parent scopes. Every one of them resolves an id with exactly the semantics of a property read.
Run
node 06-get-player-by-id-over-a-chain.cjsSource
'use strict';
// ---------------------------------------------------------------------
// PROTO-DOJO · Module 1 / Exercise 6 — getPlayerById over a
// [[Prototype]] chain
//
// GOAL: Capstone: write a roster lookup that resolves ids the way
// the language itself resolves properties — closest level
// first, follow the hidden link upward, stop at the first
// match, hand back null when there is nothing left to climb —
// by walking the chain by hand with Object.getPrototypeOf.
// CONCEPT: A read like `scope.players` only ever lands on the CLOSEST
// `players` array (shadowing!). Arrays never merge across
// levels — to search every roster you have to iterate: current
// object → Object.getPrototypeOf → … → null, and at each stop
// inspect that level's OWN array (Object.hasOwn) so no roster
// is scanned twice.
// HINT: for (let o = scope; o !== null; o = Object.getPrototypeOf(o)) {
// … }
// MIRRORS: Registries that cascade in layers — a match's own roster,
// the party the match belongs to, the campaign that owns every
// party; plugin hooks that fall back to app-level then
// framework-level handlers; DI containers with parent scopes.
// Every one of them resolves an id with exactly the semantics
// of a property read.
//
// Run: node 06-get-player-by-id-over-a-chain.cjs
// ---------------------------------------------------------------------
const assert = require('node:assert');
const campaignScope = {
scopeName: 'campaign',
players: [
{ id: 'mage', name: 'Mage' },
{ id: 'knight', name: 'Knight — campaign default build' },
],
};
const partyScope = Object.create(campaignScope);
partyScope.scopeName = 'party';
partyScope.players = [
{ id: 'knight', name: 'Knight — party build' }, // shadows the campaign one
{ id: 'rogue', name: 'Rogue' },
];
const gameScope = Object.create(partyScope);
gameScope.scopeName = 'match-42';
gameScope.players = [{ id: 'knight-dummy', name: 'Knight training dummy' }];
// -- TODO 1 -----------------------------------------------------------
// How many objects does gameScope's chain hold — gameScope included —
// before
// the hidden link bottoms out at null? Remember the object that every plain
// object literal quietly chains to. Replace '???' with your count.
const objectsOnChain = '???';
// -- TODO 2 -----------------------------------------------------------
// resolvePlayer has to reproduce property lookup by hand. At the moment it
// consults ONE roster — whichever array the single `scope.players` READ
// lands
// on — so any player registered a level higher is invisible. Walk the
// chain:
// at every level search that level's OWN `players` array (Object.hasOwn
// — a
// bare `scope.players` at each step would keep landing on the same closest
// array), return the first match, and return null once the chain is used up.
function resolvePlayer(scope, id) {
return scope.players.find((p) => p.id === id) ?? null;
}
// -- checks -----------------------------------------------------------
let walked = 0;
for (let o = gameScope; o !== null; o = Object.getPrototypeOf(o)) walked += 1;
assert.strictEqual(
objectsOnChain,
walked,
'TODO 1: gameScope → partyScope → campaignScope → …one more object you never created… → null'
);
assert.strictEqual(resolvePlayer(gameScope, 'knight-dummy').name, 'Knight training dummy',
'own scope: the closest roster is searched first');
assert.ok(
resolvePlayer(gameScope, 'rogue'),
"TODO 2: rogue is registered on the PARTY's own roster — one hidden link above the match. Walk up."
);
assert.strictEqual(resolvePlayer(gameScope, 'rogue').name, 'Rogue');
assert.strictEqual(
resolvePlayer(gameScope, 'knight').name,
'Knight — party build',
'shadowing: both party and campaign register a knight — the CLOSEST one must win, exactly like property reads'
);
assert.strictEqual(resolvePlayer(gameScope, 'mage').name, 'Mage',
'falls through two links to the campaign roster');
assert.strictEqual(resolvePlayer(gameScope, 'paladin'), null,
'exhausted chain → null, no throw (mind the level that has no own players array at all)');
assert.strictEqual(resolvePlayer(campaignScope, 'rogue'), null,
'lookup only ever walks UP the chain, never down to children');
console.log('PASS — 06-get-player-by-id-over-a-chain: you re-implemented property resolution');