Map, Set and Weak Collections

Plain objects are records with a known shape. When you need a dictionary (unknown keys, any key type), a bag of unique values, or side-data that must not leak — the language gives purpose-built collections: Map, Set, and their weak cousins, all built on one shared idea, the iteration protocol.

Why not just objects?

The dictionary traps

const counts = {};              // a dictionary? Three surprises:
console.log(counts.constructor); // ƒ Object() — inherited keys leak in
counts[1] = "one";               // numeric keys become "1" — strings only, really
console.log(Object.keys(counts).length); // must count by hand
// + no reliable size, and keys collide with names like "toString"

Map fixes all three: any key type (objects included), a real size, and no inherited keys.

Map

set, get, has, delete — and iteration in insertion order

const sessions = new Map();
const client = { id: 1 };       // an OBJECT as key — impossible in a plain object
sessions.set(client, 420);      // attach data to an object, no key collision
sessions.set("lobby", 90);

console.log(sessions.size);            // 2
console.log(sessions.get(client));     // 420
for (const [key, value] of sessions) { // iterates in INSERTION order
  console.log(key, value);
}

Set

Unique values, fast membership

const tags = ["js", "web", "js", "async", "web"];
console.log([...new Set(tags)]);      // [ 'js', 'web', 'async' ] — the dedup idiom
const seen = new Set();
seen.add("x");
console.log(seen.has("x"));           // true — constant-time, even for big sets

Weak collections

Side data that cannot leak

const metadata = new WeakMap();       // keys may only be OBJECTS, held weakly
const domNode = { tagName: "section" };
metadata.set(domNode, { visits: 3 });
console.log(metadata.has(domNode));   // true
// no .size, no iteration — BY DESIGN: entries whose key object is garbage-
// collected vanish on their own. Attach data to DOM nodes without leak risk.

WeakSet works the same for "has this object been seen?" flags. The rule: strong collections (Map/Set) when the collection owns the data; weak ones when the objects elsewhere own their own lifetime.

The iteration protocol

One interface for everything iterable

for...of works on arrays, strings, Map, Set — anything with a [Symbol.iterator] method returning an object whose next() yields { value, done }. You can implement it too:

const range = {
  from: 1, to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    return { next: () => ({ value: current, done: current++ > this.to }) };
  },
};
for (const n of range) console.log(n); // 1, 2, 3 — your own iterable

Generators (lesson 21) produce the same interface with far less ceremony — but the protocol explains why for...of refuses plain objects: they have no iterator.

Choosing the right collection

The decision table

NeedReach for
Fixed shape, named fieldsplain object (record)
Unknown/untrusted keys, any key type, sizeMap
Uniqueness, fast membershipSet
Side-data tied to objects' lifetimesWeakMap/WeakSet

Practice: the tag counter

The task

Run foundations/17_collections_lab.js with node, predicting each output. Then build a tag counter: count occurrences of tags in an array with a Map, and print them sorted by count.

The checklist

  • You can name three things a Map does that a plain object cannot.
  • You use [...new Set(items)] without looking it up.
  • You can explain why a WeakMap has no size.
  • You can say which collection fits a given need from the table, from memory.