// ───────────────────────────────────────────────────────────────────────────── // JS OBJECTS, DESTRUCTURING, SPREAD, OPTIONAL CHAINING // ───────────────────────────────────────────────────────────────────────────── // COPY : { ...o } shallow; structuredClone(o) deep (plain data; no functions) // READ : o?.a?.b (optional chaining), o.a ?? 'default' (nullish — not ||) // ITERATE : Object.keys / values / entries / fromEntries; for..in walks inherited // TRAP : {} === {} is false; JSON.stringify drops undefined/functions, turns // NaN/Infinity into null, loses Map/Set/Date type; key order rules // SAY : "shallow vs deep copy", "immutable update", "reference equality", // "property descriptor", "computed key", "shorthand property" // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); const user = { id: 7, name: 'Ada', tags: ['x'], address: { city: 'NYC' } }; // ── 1. Destructuring: defaults, rename, nesting, rest, skipping, swap, in params const { name, role = 'guest', address: { city }, id: userId } = user; assert.deepEqual([name, role, city, userId], ['Ada', 'guest', 'NYC', 7]); const { tags, ...withoutTags } = user; assert.deepEqual(tags, ['x']); assert.equal('tags' in withoutTags, false); const [first, , third = 'dflt', ...rest] = [1, 2, undefined, 4, 5]; assert.deepEqual([first, third, rest], [1, 'dflt', [4, 5]]); let p = 1, q = 2; [p, q] = [q, p]; assert.deepEqual([p, q], [2, 1]); const area = ({ w, h = w }) => w * h; // destructure in the parameter list assert.equal(area({ w: 3 }), 9); const [[r, c]] = [[1, 2]]; // nested array destructure assert.equal(r + c, 3); for (const [k, v] of Object.entries({ a: 1 })) assert.equal(`${k}${v}`, 'a1'); // ── 2. ?. and ?? (?? treats only null/undefined as missing; || also 0, '', false) assert.equal(user.address?.zip?.code, undefined); assert.equal(user.getName?.(), undefined); // optional call assert.equal(user.tags?.[0], 'x'); // optional index assert.equal(user.missing ?? 'd', 'd'); assert.equal(0 ?? 5, 0); assert.equal(0 || 5, 5); assert.equal('' ?? 'x', ''); let cfg = { retries: 0 }; cfg.retries ??= 3; // only assigns if null/undefined cfg.timeout ||= 1000; assert.deepEqual(cfg, { retries: 0, timeout: 1000 }); // ── 3. Spread / merge / immutable update const updated = { ...user, name: 'Grace', address: { ...user.address, city: 'LA' } }; assert.equal(updated.name, 'Grace'); assert.equal(user.name, 'Ada'); // original untouched assert.equal(user.address.city, 'NYC'); assert.deepEqual({ ...{ a: 1, b: 2 }, ...{ b: 3 } }, { a: 1, b: 3 }); // later wins const deep = structuredClone(user); deep.tags.push('y'); assert.deepEqual(user.tags, ['x']); assert.equal(Object.assign({}, { a: 1 }).a, 1); // older spelling of spread-merge // ── 4. Keys / computed keys / shorthand / methods const key = 'dyn'; const o = { [key]: 1, [`${key}2`]: 2, name, greet() { return `hi ${this.name}`; } }; assert.deepEqual(Object.keys(o), ['dyn', 'dyn2', 'name', 'greet']); assert.equal(o.greet(), 'hi Ada'); assert.deepEqual(Object.values({ a: 1, b: 2 }), [1, 2]); assert.deepEqual(Object.entries({ a: 1 }).map(([k, v]) => `${k}=${v}`), ['a=1']); assert.deepEqual(Object.fromEntries([['a', 1], ['b', 2]]), { a: 1, b: 2 }); const mapValues = (obj, f) => Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, f(v)])); assert.deepEqual(mapValues({ a: 1, b: 2 }, v => v * 10), { a: 10, b: 20 }); const pick = (obj, ...ks) => Object.fromEntries(ks.filter(k => k in obj).map(k => [k, obj[k]])); assert.deepEqual(pick(user, 'id', 'nope'), { id: 7 }); const invertObj = obj => Object.fromEntries(Object.entries(obj).map(([k, v]) => [v, k])); assert.deepEqual(invertObj({ a: 'x' }), { x: 'a' }); // ── 5. Existence checks assert.equal('name' in user, true); assert.equal(Object.hasOwn(user, 'name'), true); assert.equal(user.nope === undefined, true); // but a key can hold undefined explicitly assert.equal(Object.keys({ a: undefined }).length, 1); const isEmpty = obj => Object.keys(obj).length === 0; assert.equal(isEmpty({}), true); delete o.dyn2; assert.equal('dyn2' in o, false); // ── 6. Equality is by reference; compare by value via JSON (ordered) or deepEqual assert.equal({ a: 1 } === { a: 1 }, false); assert.equal(JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 }), false); // order matters assert.deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 }); // deepEqual doesn't care const sameShallow = (x, y) => Object.keys(x).length === Object.keys(y).length && Object.keys(x).every(k => x[k] === y[k]); assert.equal(sameShallow({ a: 1, b: 2 }, { b: 2, a: 1 }), true); // ── 7. JSON edge cases assert.equal(JSON.stringify({ a: undefined, f() {}, n: NaN, d: new Date(0) }), '{"n":null,"d":"1970-01-01T00:00:00.000Z"}'); assert.equal(JSON.stringify([undefined]), '[null]'); assert.equal(JSON.stringify(new Map([[1, 2]])), '{}'); // Map/Set serialize to {} assert.equal(JSON.stringify({ b: 1, a: 2 }, null, 0), '{"b":1,"a":2}'); assert.equal(JSON.stringify({ b: 1, a: 2 }, ['a']), '{"a":2}'); // replacer allowlist assert.deepEqual(JSON.parse('{"x":"1"}', (k, v) => (k === 'x' ? Number(v) : v)), { x: 1 }); // reviver assert.throws(() => JSON.parse('{bad'), SyntaxError); assert.equal(JSON.stringify('he said "hi"'), '"he said \\"hi\\""'); // ── 8. Freezing / sealing (shallow) const frozen = Object.freeze({ a: 1, inner: { b: 2 } }); assert.throws(() => { frozen.a = 2; }, TypeError); // throws only in strict mode frozen.inner.b = 3; // shallow: nested still mutable assert.equal(frozen.inner.b, 3); assert.equal(Object.isFrozen(frozen), true); // ── 9. Getters / setters / defineProperty const temp = { _c: 0, get f() { return this._c * 9 / 5 + 32; }, set f(v) { this._c = (v - 32) * 5 / 9; }, }; temp.f = 212; assert.equal(temp._c, 100); Object.defineProperty(temp, 'hidden', { value: 42, enumerable: false }); assert.equal(temp.hidden, 42); assert.equal(Object.keys(temp).includes('hidden'), false); console.log('ok: javascript/objects-destructuring.js');