Map, Set, and Built-in Collections
This chapter covers the built-in keyed and grouped collections: Map, Set, WeakMap, WeakSet, and how they differ from plain objects and arrays. It assumes you are comfortable with objects, property lookup, the prototype chain, iteration protocols, reference semantics, and garbage collection basics.
Why Map instead of an object
A plain object is a hash-like structure, but it inherits Object.prototype and coerces every key to a string (or symbol). Map is a true dictionary: keys can be any value, including objects, functions, NaN, and undefined, and insertion order is guaranteed for all keys.
const byId = new Map();
const key = { id: 1 };
byId.set(key, 'data for this object');
console.log(byId.get(key)); // 'data for this object'
console.log(byId.get({ id: 1 })); // undefined — identity, not structural, equality
console.log(byId.size); // 1Key differences from objects:
| Concern | Object | Map |
|---|---|---|
| Key types | strings and symbols only | any value, compared by SameValueZero |
| Prototype pollution | `__proto__`, `toString` collisions possible | none; keys are stored in an internal slot |
| Size | manual (`Object.keys().length`) | `.size` |
| Iteration order | mostly insertion order, but integer-like keys come first | strict insertion order |
| Direct iteration | not iterable | iterable; `[...map]` yields `[key, value]` pairs |
| Serialization | `JSON.stringify` works | not natively serializable |
| Performance on frequent adds/deletes | often slower for large, mutating maps | tuned for insert/delete workloads |
The SameValueZero algorithm differs from === only in treating NaN as equal to itself, which is why a Map can use NaN as a working key.
When an object is still the right choice
Prefer an object when: keys are a fixed schema, the data crosses JSON.stringify, or you rely on property access syntax and destructuring. Prefer Map when keys are dynamic, non-string, or the collection mutates heavily.
Set
Set is a collection of unique values under the same key semantics as Map — in fact, internally a Set is modeled as a Map where each entry has undefined as its value.
const tags = new Set(['js', 'async', 'js']);
console.log(tags.size); // 2
console.log(tags.has('async')); // true
tags.delete('js');
for (const t of tags) console.log(t); // 'async'
// Deduplicating values from an array
const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]Sets support the usual algebra operations, which you compose yourself since there is no union method:
const a = new Set([1, 2, 3]);
const b = new Set([3, 4]);
const union = new Set([...a, ...b]); // {1,2,3,4}
const inter = new Set([...a].filter(x => b.has(x))); // {3}
const diff = new Set([...a].filter(x => !b.has(x)));// {1,2}has on a Set and get on a Map are approximately O(1) — implementations use hash tables keyed by value identity — versus O(n) for includes on arrays. Replacing array-membership checks with a Set in a hot loop is a common, effective optimization.
WeakMap and WeakSet
WeakMap and WeakSet hold their keys weakly: a key that is only referenced by the collection does not prevent garbage collection, and the entry vanishes when the key is collected. This is the tool for attaching metadata to objects you do not own, without leaking memory.
const metadata = new WeakMap();
let node = { name: 'dom-node' };
metadata.set(node, { visits: 3 });
node = null; // the entry becomes collectible; metadata will not leak itThe constraints follow from the implementation: keys must be objects (or non-registered symbols), the collections are not iterable, and there is no .size, because the runtime does not expose when collection happens. You cannot enumerate a WeakMap — enumeration would have to keep weak entries alive to show them.
Typical uses: private per-instance state, caches keyed by object, and associating data with DOM nodes or library objects.
Internals and gotchas
MapandSetiteration is insertion-ordered, but deleting and re-adding a key moves it to the end of the iteration order.- Identity, not structure, governs keys: two equal-looking objects are different keys. Use a serialized string key or a composite key type if you need structural keying.
- Mutating an object does not change its hash — identity is the key — so mutable keys are safe, unlike in some languages.
Map/Set/WeakMapcannot be cloned bystructuredClone's handling of WeakMaps (weak entries are dropped) and require manual reconstruction forJSON.- Per spec,
Mapoperations have no specified complexity bound, but in practice all mainstream engines are hash-table backed; do not assume sorted order.
Decision criteria
- Fixed schema, JSON boundary → plain object.
- Dynamic or non-string keys, heavy mutation →
Map. - Uniqueness or fast membership →
Set. - Metadata attached to objects you don't own, or a cache that must not retain →
WeakMap/WeakSet.
Exercise
You need to count how many times each word appears, then serialize the result to JSON. Map or object, and why?
Show answer
Either works, but an object (or a Map converted at the boundary) is the pragmatic choice if the immediate next step is JSON serialization, which Map does not support natively. Use a Map during processing for O(1) updates and clean key semantics, then Object.fromEntries(map) before serializing.
Exercise
Why can't WeakMap keys be primitives like strings?
Show answer
Primitives are values, not references, so there is nothing to hold weakly — a primitive key would either keep the entry alive forever or require an arbitrary rule for "collecting" a value. Requiring object keys gives the GC a clear reference to observe.
Spotted a mistake, or code that does not work?