js-dojo

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

Source

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

Solution