Map, Set and Weak 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
| Need | Reach for |
|---|---|
| Fixed shape, named fields | plain object (record) |
| Unknown/untrusted keys, any key type, size | Map |
| Uniqueness, fast membership | Set |
| Side-data tied to objects' lifetimes | WeakMap/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
Mapdoes that a plain object cannot. - You use
[...new Set(items)]without looking it up. - You can explain why a
WeakMaphas nosize. - You can say which collection fits a given need from the table, from memory.