4 hasOwn vs in vs for-in + enumerability
GOAL
Know precisely which properties each membership test can see: Object.hasOwn → OWN only (enumerable or not) `in` → own + INHERITED (enumerable or not) for-in → own + inherited, but ENUMERABLE only Object.keys → own + enumerable only
CONCEPT
Enumerability is a flag stamped on each property (defineProperty). It decides visibility in for-in / Object.keys / spread — never existence.
HINT
A save routine that for-ins over a settings object chained to its defaults sweeps every inherited default into the save file; Object.hasOwn is the filter that keeps only what belongs here.
MIRRORS
Writing a player's settings into a save slot: the tweaks they made must be persisted, engine defaults must NOT — and internal plumbing such as a live input socket handle must never hit disk (which is precisely why real engines define such props non-enumerable).
Run
node 04-own-vs-in-vs-forin.cjsSource
'use strict';
// ---------------------------------------------------------------------
// PROTO-DOJO · Module 1 / Exercise 4 — hasOwn vs `in` vs for-in +
// enumerability
//
// GOAL: Know precisely which properties each membership test can
// see: Object.hasOwn → OWN only (enumerable or not) `in`
// → own + INHERITED (enumerable or not) for-in → own
// + inherited, but ENUMERABLE only Object.keys → own +
// enumerable only
// CONCEPT: Enumerability is a flag stamped on each property
// (defineProperty). It decides visibility in for-in /
// Object.keys / spread — never existence.
// HINT: A save routine that for-ins over a settings object chained
// to its defaults sweeps every inherited default into the save
// file; Object.hasOwn is the filter that keeps only what
// belongs here.
// MIRRORS: Writing a player's settings into a save slot: the tweaks
// they made must be persisted, engine defaults must NOT — and
// internal plumbing such as a live input socket handle must
// never hit disk (which is precisely why real engines define
// such props non-enumerable).
//
// Run: node 04-own-vs-in-vs-forin.cjs
// ---------------------------------------------------------------------
const assert = require('node:assert');
const settingsDefaults = { vsync: true, autosave: true, particleBudget: 5000 };
const gameSettings = Object.create(settingsDefaults); // this match's settings chain to the defaults
gameSettings.hero = 'knight';
gameSettings.difficulty = 'hard';
Object.defineProperty(gameSettings, '_inputHandle', {
value: { socket: 'wss://arena.example/input' },
enumerable: false, // internal plumbing — must never end up in a save slot
writable: true,
});
// what for-in ACTUALLY sees (computed for you — study it):
const forInKeys = [];
for (const key in gameSettings) forInKeys.push(key);
// -- TODO 1: predict — replace every '???' with true or false ---------
const quiz = {
heroIsOwn: '???', // Object.hasOwn(gameSettings, 'hero')
vsyncIsOwn: '???', // Object.hasOwn(gameSettings, 'vsync')
vsyncIn: '???', // 'vsync' in gameSettings
inputHandleIn: '???', // '_inputHandle' in gameSettings
inputHandleInForIn: '???', // forInKeys.includes('_inputHandle')
vsyncInForIn: '???', // forInKeys.includes('vsync')
};
assert.strictEqual(quiz.heroIsOwn, Object.hasOwn(gameSettings, 'hero'),
'TODO 1: hero was assigned directly onto gameSettings');
assert.strictEqual(quiz.vsyncIsOwn, Object.hasOwn(gameSettings, 'vsync'),
'TODO 1: hasOwn does NOT walk the chain — vsync lives on settingsDefaults');
assert.strictEqual(quiz.vsyncIn, 'vsync' in gameSettings,
'TODO 1: `in` DOES walk the chain — inherited counts');
assert.strictEqual(quiz.inputHandleIn, '_inputHandle' in gameSettings,
'TODO 1: `in` ignores enumerability — non-enumerable props still exist');
assert.strictEqual(quiz.inputHandleInForIn, forInKeys.includes('_inputHandle'),
'TODO 1: for-in skips non-enumerable props — that is the whole point of the flag');
assert.strictEqual(quiz.vsyncInForIn, forInKeys.includes('vsync'),
'TODO 1: for-in walks the chain too — inherited ENUMERABLE props show up');
// -- TODO 2 -----------------------------------------------------------
// serializeForSaveSlot should persist ONLY what the player actually changed on
// THIS game. The naive for-in below sweeps every inherited default into the
// save file (and would write engine defaults back as if the player chose them).
// Fix it to keep own, enumerable props only — filter with
// Object.hasOwn, or
// swap for-in for Object.keys / Object.entries.
function serializeForSaveSlot(settings) {
const payload = {};
for (const key in settings) {
payload[key] = settings[key];
}
return payload;
}
assert.deepStrictEqual(
serializeForSaveSlot(gameSettings),
{ hero: 'knight', difficulty: 'hard' },
'TODO 2: the save payload must contain the player\'s own tweaks only — no inherited defaults, no _inputHandle'
);
console.log('PASS — 04-own-vs-in-vs-forin: hasOwn / in / for-in / enumerability sorted');