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
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 automaticallySet 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
.sizeon a weak collection → not possible, use Set/Map instead
Comparison table
| Feature | Set | Map | WeakSet | WeakMap |
|---|---|---|---|---|
| Stored data | Values only | Key-value pairs | Objects only | Object keys, any values |
| Key types | Any value | Any value | Objects only | Objects only |
| Iteration | Yes | Yes | No | No |
.size property | Yes | Yes | No | No |
| Garbage collection | No (strong refs) | No (strong refs) | Yes (weak refs) | Yes (weak refs) |
| Typical use | Unique lists | Non-string key maps | Visited object flags | Node 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:
const map = new Map();
map.set({ id: 1 }, 'data');
console.log(map.has({ id: 1 })); // false - different object referenceMap 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:
const wm = new WeakMap();
wm.set({}, 1);
console.log(wm.size); // undefined
for (const [k, v] of wm) {} // TypeError: wm is not iterableThere is no .size and no way to list entries. If you need a count, track it separately.
Assuming WeakMap prevents all memory leaks automatically:
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 upWeak 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
_.uniqfor 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
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); // 2Set 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
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
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 collectorWith 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 readyA concise answer to help you respond confidently on this topic during an interview.