Skip to main content

Set, Map, WeakSet and WeakMap in JavaScript

Set, Map, WeakSet and WeakMap are four built-in JavaScript collection types that each solve a different storage problem.

Theory

TL;DR

  • Set is a list that never holds duplicates. Map stores key-value pairs where keys can be objects, not just strings.
  • WeakSet and WeakMap hold weak references to objects, so the garbage collector removes entries when nothing else points to those objects.
  • Main split: Set/Map keep strong references and support iteration. WeakSet/WeakMap trade iteration for automatic memory cleanup.
  • Use Set for deduplication, Map when you need non-string keys, WeakMap for DOM node metadata or caches.

Quick example

javascript
const set = new Set([1, 2, 2, 3]); // Set {1, 2, 3} - duplicates dropped const map = new Map(); map.set('name', 'Alice'); map.set({id: 1}, 'object as key'); // any type works as key let node = document.querySelector('#btn'); const weakMap = new WeakMap(); weakMap.set(node, { clickCount: 0 }); // attach data to DOM node node = null; // entry will be GC'd automatically

Set drops the duplicate 2 on creation. Map accepts an object as a key, which plain objects cannot do. WeakMap ties data to a DOM node and releases it when the node is gone.

Key difference

Set and Map hold strong references. An object stored as a Map key stays in memory as long as that Map exists, even if no other code references it. WeakSet and WeakMap use weak references: if the only pointer to an object is a WeakMap key, the garbage collector treats that object as unreachable and removes the entry. No manual cleanup needed.

When to use

  • Remove duplicates or check membership quickly → Set (O(1) lookup vs array's O(n))
  • Store values by non-string keys, like objects or Symbols → Map
  • Track per-element state for DOM nodes without memory leaks → WeakMap
  • Flag which objects have been visited in an algorithm, like cycle detection → WeakSet
  • Need to iterate or check .size on a weak collection → not possible, use Set/Map instead

Comparison table

FeatureSetMapWeakSetWeakMap
Stored dataValues onlyKey-value pairsObjects onlyObject keys, any values
Key typesAny valueAny valueObjects onlyObjects only
IterationYesYesNoNo
.size propertyYesYesNoNo
Garbage collectionNo (strong refs)No (strong refs)Yes (weak refs)Yes (weak refs)
Typical useUnique listsNon-string key mapsVisited object flagsNode caches, metadata

How it works internally

V8 implements Set and Map as hash tables with an ordered linked list on top, which is why insertion order is preserved during iteration. Equality uses SameValueZero rather than ===. The practical difference: NaN === NaN is false in JavaScript, but SameValueZero treats two NaN values as identical, so new Set([NaN, NaN]) gives Set {NaN} with one element.

WeakSet and WeakMap use a structure called an ephemeron table. An ephemeron is a key-value pair where the value is kept alive only if the key is reachable from outside the table. During a GC cycle, if the engine finds a WeakMap key has no external references, the entry is dropped. Iteration is excluded by design: iterating would require holding strong references to all keys temporarily, which blocks the garbage collector.

Common mistakes

Using object literals as Map keys and expecting them to match:

javascript
const map = new Map(); map.set({ id: 1 }, 'data'); console.log(map.has({ id: 1 })); // false - different object reference

Map compares keys by reference (SameValueZero), not deep equality. Two {id: 1} objects are distinct in memory. Store the reference in a variable if you need to retrieve the value later.

Expecting .size or iteration on WeakMap or WeakSet:

javascript
const wm = new WeakMap(); wm.set({}, 1); console.log(wm.size); // undefined for (const [k, v] of wm) {} // TypeError: wm is not iterable

There is no .size and no way to list entries. If you need a count, track it separately.

Assuming WeakMap prevents all memory leaks automatically:

javascript
let obj = { data: 'large payload' }; const cache = new WeakMap(); cache.set(obj, 'computed result'); // obj is still referenced above - no GC happens yet // Only when ALL strong refs to obj are gone will the WeakMap entry be cleaned up

Weak references work only when nothing else holds the object. A closure, an array, or another variable still pointing to obj keeps the WeakMap entry alive.

Using a plain object instead of Map for non-string keys:

Plain objects coerce all keys to strings. obj[{id:1}] becomes obj["[object Object]"]. Map keeps the actual type, so map.set({id:1}, value) stores the object reference without any coercion.

Real-world usage

  • React uses WeakMap in its Fiber reconciler to associate effects with fiber nodes without blocking their collection.
  • Node.js AsyncHooks uses WeakMap to track async resources so finished operations can be GC'd.
  • Lodash uses Set inside _.uniq for O(n) array deduplication.
  • Preact uses WeakSet to track which hooks have been committed.
  • Express uses Map for middleware parameter registries where insertion order matters.

Follow-up questions

Q: What is SameValueZero and how does it differ from ===?
A: SameValueZero treats +0 and -0 as equal, and NaN as equal to NaN. Regular === returns false for NaN === NaN. So new Set([NaN, NaN]) gives Set {NaN} with one element.

Q: Why can't you iterate over WeakSet or WeakMap?
A: Iteration requires the engine to enumerate all keys, which means holding strong references to them during the loop. That blocks garbage collection, so the design deliberately excludes iteration.

Q: Can WeakMap keys be primitives like strings or numbers?
A: No. Primitives are values, not references. GC tracks object references, so only objects can be weak keys. Passing a string throws a TypeError.

Q: Set .has() vs Array.includes() - is there a real performance difference?
A: Set uses a hash table, so .has() is O(1) on average. Array.includes() scans linearly, O(n). For large collections or repeated lookups, Set is noticeably faster.

Q (senior level): How would you implement an LRU cache using WeakMap?
A: WeakMap handles the key-to-value association and automatic cleanup when keys are GC'd. You still need a doubly-linked list or an access-ordered Map to track eviction order, because WeakMap has no iteration. On each access, move the entry to the front. On capacity overflow, evict the tail. If a key gets GC'd externally, its entry disappears from WeakMap without touching the linked list.

Examples

Deduplication and membership check with Set

javascript
const visited = new Set(); function processPage(url) { if (visited.has(url)) { console.log('Already processed:', url); return; } visited.add(url); console.log('Processing:', url); } processPage('https://example.com'); // Processing: https://example.com processPage('https://example.com'); // Already processed: https://example.com processPage('https://other.com'); // Processing: https://other.com console.log(visited.size); // 2

Set replaces the typical if (array.indexOf(url) !== -1) pattern and cuts lookup time to O(1). I used this exact pattern for deduplicating navigation history in a single-page app - switching from an array to Set cut lookup time on large histories from noticeable to instant.

Map with object keys for per-request context

javascript
const requestContext = new Map(); function handleRequest(req) { requestContext.set(req, { startTime: Date.now(), userId: req.headers['x-user-id'] }); } function logRequest(req) { const ctx = requestContext.get(req); console.log(`Duration: ${Date.now() - ctx.startTime}ms`); requestContext.delete(req); }

With a plain object, req as a key would become "[object Object]" and every request would overwrite the same entry. Map preserves the reference and keeps each request's context separate.

WeakMap for DOM node state without memory leaks

javascript
const clickCounts = new WeakMap(); document.querySelectorAll('button').forEach(button => { clickCounts.set(button, 0); button.addEventListener('click', () => { const count = (clickCounts.get(button) || 0) + 1; clickCounts.set(button, count); console.log(`Clicked ${count} times`); }); }); // When a button is removed from the DOM and no other references exist, // its entry in clickCounts is automatically cleaned up by the garbage collector

With a regular Map here, every removed button would stay in memory as long as the Map lived. WeakMap makes the cleanup automatic.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?