// ───────────────────────────────────────────────────────────────────────────── // JS MAP / SET / WEAKMAP vs PLAIN OBJECT — counting, grouping, lookups // ───────────────────────────────────────────────────────────────────────────── // MAP : any key type, insertion order, .size, no prototype keys. Use it as // your default dictionary in interviews. // SET : unique values, O(1) has/add/delete, insertion order. // OBJECT : keys become STRINGS; integer-like keys get reordered; inherits // toString etc. Use Object.create(null) or Map if keys are user input. // TRAP : Map/Set compare objects/arrays BY REFERENCE — stringify compound keys. // SAY : "hash map", "O(1) average lookup", "frequency map", "seen set", // "insertion-ordered", "WeakMap for metadata without leaks" // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── 1. The counting idiom const counts = new Map(); for (const w of ['a', 'b', 'a', 'c', 'a']) counts.set(w, (counts.get(w) ?? 0) + 1); assert.equal(counts.get('a'), 3); assert.equal(counts.get('zzz'), undefined); // miss -> undefined; hence ?? 0 assert.equal(counts.has('b'), true); assert.equal(counts.size, 3); assert.equal(counts.delete('c'), true); assert.equal(counts.delete('c'), false); // ── 2. Iterating / converting assert.deepEqual([...counts.keys()], ['a', 'b']); assert.deepEqual([...counts.values()], [3, 1]); assert.deepEqual([...counts], [['a', 3], ['b', 1]]); // same as [...counts.entries()] for (const [k, v] of counts) assert.equal(typeof k + typeof v, 'stringnumber'); assert.deepEqual(Object.fromEntries(counts), { a: 3, b: 1 }); assert.deepEqual(new Map(Object.entries({ x: 1 })).get('x'), 1); const maxKey = [...counts].reduce((best, cur) => (cur[1] > best[1] ? cur : best))[0]; assert.equal(maxKey, 'a'); // ── 3. Grouping const words = ['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; const groups = new Map(); for (const w of words) { const key = [...w].sort().join(''); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(w); } assert.deepEqual([...groups.values()], [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]); // Object.groupBy / Map.groupBy exist in Node 21+ — don't rely on them in an unknown runtime. // ── 4. Set const seen = new Set([1, 2, 2, 3]); assert.equal(seen.size, 3); seen.add(4).add(4); // add returns the set (chainable) seen.delete(1); assert.equal(seen.has(2), true); assert.deepEqual([...seen], [2, 3, 4]); const uniq = xs => [...new Set(xs)]; assert.deepEqual(uniq([3, 1, 3]), [3, 1]); const hasDuplicates = xs => new Set(xs).size !== xs.length; assert.equal(hasDuplicates([1, 2, 1]), true); // Set algebra (Node 22 has .union/.intersection; portable versions:) const A = new Set([1, 2, 3]), B = new Set([2, 3, 4]); assert.deepEqual([...A].filter(x => B.has(x)), [2, 3]); // intersection assert.deepEqual([...A].filter(x => !B.has(x)), [1]); // difference assert.deepEqual([...new Set([...A, ...B])], [1, 2, 3, 4]); // union // ── 5. Compound keys: by reference! const visited = new Set(); visited.add(`${1},${2}`); assert.equal(visited.has('1,2'), true); assert.equal(new Set([[1, 2]]).has([1, 2]), false); // different array object const pointKey = (r, c) => r * 100000 + c; // numeric key alternative if bounds are known assert.equal(pointKey(1, 2), 100002); // ── 6. Map as an LRU (insertion order + delete/re-set trick) class LRU { #cap; #m = new Map(); constructor(cap) { this.#cap = cap; } get(k) { if (!this.#m.has(k)) return -1; const v = this.#m.get(k); this.#m.delete(k); this.#m.set(k, v); // move to most-recent (end) return v; } put(k, v) { this.#m.delete(k); this.#m.set(k, v); if (this.#m.size > this.#cap) this.#m.delete(this.#m.keys().next().value); // evict oldest (first) } } const lru = new LRU(2); lru.put(1, 'a'); lru.put(2, 'b'); lru.get(1); lru.put(3, 'c'); assert.equal(lru.get(2), -1); assert.equal(lru.get(1), 'a'); // ── 7. Plain object pitfalls const obj = {}; obj[1] = 'one'; assert.equal(obj['1'], 'one'); // key is the string "1" assert.deepEqual(Object.keys({ b: 1, a: 2 }), ['b', 'a']); // insertion order... assert.deepEqual(Object.keys({ 2: 'x', 1: 'y' }), ['1', '2']); // ...except integer-like keys sort ascending assert.equal('toString' in obj, true); // inherited! assert.equal(Object.hasOwn(obj, 'toString'), false); // Node 16.9+; or obj.hasOwnProperty.call const dict = Object.create(null); // no prototype at all assert.equal('toString' in dict, false); assert.equal(Object.keys({}).length, 0); // "is empty" check // ── 8. WeakMap / WeakSet — keys must be objects, not enumerable, GC-friendly (private data, caches) const meta = new WeakMap(); const user = { id: 1 }; meta.set(user, { lastSeen: 123 }); assert.equal(meta.get(user).lastSeen, 123); assert.equal(meta.has({ id: 1 }), false); // different object // ── 9. Map vs Object: when the interviewer asks // Map: frequent add/remove, non-string keys, need size, need order, keys from user input. // Object: fixed shape records, JSON in/out, destructuring, property access syntax. console.log('ok: javascript/map-set.js');