JavaScript Map, Set, WeakMap, and WeakSet: When to Use Which
Stop using plain objects for everything. Here's what to use when.
Quick Comparison
┌───────────┬──────────┬──────────┬──────────┬────────────┐
│ │ Map │ Set │ WeakMap │ WeakSet │
├───────────┼──────────┼──────────┼──────────┼────────────┤
│ Key type │ Any value│ N/A │ Objects │ N/A │
│ Value │ Any value│ Keys only│ Any value│ Objects │
│ Iterable?│ ✅ │ ✅ │ ❌ │ ❌ │
│ Size prop│ ✅ │ ✅ │ ❌ │ ❌ │
│ GC safe │ No │ No │ Yes │ Yes │
│ Use case │ Lookup │ Unique │ Private │ Membership │
└───────────┴──────────┴──────────┴──────────┴────────────┘
Map (Key-Value Store with Advantages Over Objects)
// ❌ Object limitations:
const obj = {};
obj['toString'] = 'oops'; // Overrides inherited method!
obj.__proto__ = 'dangerous'; // Can pollute prototype
Object.keys(obj).length; // Doesn't count non-enumerable properties
// ✅ Map advantages:
const map = new Map();
// Any type as key (not just strings/symbols!)
map.set('name', 'Alex');
map.set(42, 'the answer');
map.set(true, 'boolean key');
map.set({}, 'object key');
map.set(() => {}, 'function key');
map.set(NaN, 'NaN works!'); // NaN === NaN in Maps!
// Get values
map.get('name'); // 'Alex'
map.get('nonexistent'); // undefined
// Check existence
map.has('name'); // true
map.has('toString'); // false (no prototype pollution!)
// Size
map.size; // 6
// Delete
map.delete(42); // true
map.clear(); // Remove all
// Iterate (insertion order preserved!)
for (const [key, value] of map) {
console.log(`${key} = ${value}`);
}
// Convert to/from arrays
const arr = Array.from(map); // [[key, value], ...]
const newMap = new Map([['a', 1], ['b', 2]]);
// Practical: LRU Cache
class LRUCache {
#cache = new Map();
constructor(maxSize = 100) {
this.maxSize = maxSize;
}
get(key) {
if (!this.#cache.has(key)) return undefined;
const value = this.#cache.get(key);
this.#cache.delete(key); // Remove and re-add to mark as recently used
this.#cache.set(key, value);
return value;
}
set(key, value) {
if (this.#cache.has(key)) this.#cache.delete(key);
if (this.#cache.size >= this.maxSize) {
const oldest = this.#cache.keys().next().value;
this.#cache.delete(oldest);
}
this.#cache.set(key, value);
}
}
Set (Unique Values Only)
// Create from array (removes duplicates!)
const numbers = [1, 2, 2, 3, 3, 3, 4, 5];
const unique = [...new Set(numbers)]; // [1, 2, 3, 4, 5]
const set = new Set();
set.add('hello');
set.add('world');
set.add('hello'); // Ignored — already exists!
set.size; // 2
set.has('world'); // true
set.delete('hello'); // true
// Useful operations
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
// Union
new Set([...a, ...b]); // {1, 2, 3, 4}
// Intersection
new Set([...a].filter(x => b.has(x))); // {2, 3}
// Difference
new Set([...a].filter(x => !b.has(x))); // {1}
// Practical use cases:
// Deduplicate array of objects by property
const users = [
{ id: 1, name: 'Alex' },
{ id: 2, name: 'Sam' },
{ id: 1, name: 'Alex' }, // Duplicate!
];
const uniqueUsers = [...new Map(users.map(u => [u.id, u])).values()];
// Check for unique characters
const allUnique = str => new Set(str).size === str.length;
allUnique('abcde'); // true
allUnique('hello'); // false (l repeats)
WeakMap (Garbage-Collected Key-Value)
// Keys MUST be objects (or non-registered symbols)
// Values can be anything
// NOT iterable (can't list keys/values)
// Keys are weakly held — GC can collect them!
// Practical: Private class fields (before #private was available)
class User {
#data = new WeakMap();
constructor(name, password) {
this.name = name;
this.#data.set(this, { password }); // `this` is the key!
}
checkPassword(input) {
return this.#data.get(this)?.password === input;
}
}
// When the User instance is garbage collected,
// the password data is automatically cleaned up!
// Practical: Metadata without memory leaks
const elementData = new WeakMap();
function addMetadata(el, data) {
elementData.set(el, data);
}
// Even if you remove the element from DOM,
// the data won't prevent GC from collecting it!
// Practical: Caching expensive computations on objects
const cache = new WeakMap();
function deepClone(obj) {
if (cache.has(obj)) return cache.get(obj);
const clone = /* expensive clone operation */;
cache.set(obj, clone);
return clone;
}
// Cache automatically cleans up when original objects are GC'd!
WeakSet (Garbage-Collected Membership)
// Only stores objects
// Can't iterate or get size
// Perfect for "is this object tracked?" checks
const processed = new WeakSet();
function processItem(item) {
if (processed.has(item)) {
console.log('Already processed, skipping');
return;
}
// Process item...
processed.add(item); // Mark as processed
}
// When item is no longer referenced elsewhere,
// it's automatically removed from the WeakSet!
// Practical: DOM element tracking
const clickedElements = new WeakSet();
document.addEventListener('click', e => {
if (clickedElements.has(e.target)) {
e.target.classList.toggle('selected');
} else {
clickedElements.add(e.target);
e.target.classList.add('clicked-once');
}
});
// Practical: Prevent circular references causing memory leaks
const visited = new WeakSet();
function traverse(obj) {
if (typeof obj !== 'object' || obj === null || visited.has(obj)) return;
visited.add(obj);
for (const key of Object.keys(obj)) {
traverse(obj[key]);
}
}
Performance
// Map vs Object for frequent add/delete:
// Map is faster for large datasets, especially with frequent additions/deletions
// Benchmark (approximate):
// 10,000 entries:
// - Object: ~15ms for lookup, ~25ms for delete
// - Map: ~8ms for lookup, ~12ms for delete
// Set vs Array for membership testing:
// 10,000 items, 10,000 lookups:
// - Array.includes(): ~120ms
// - Set.has(): ~0.05ms (2400x faster!)
// Always use Set for frequent "is X in collection?" checks!
Decision Guide
Need key-value pairs?
├── Keys are strings/symbols → Object {} or Map
│ ├── Need JSON serialization → Object {}
│ ├── Need any-type keys → **Map**
│ ├── Need ordered iteration → **Map**
│ └── Need prototype-free → **Map**
│
├── Keys are objects + auto-GC → **WeakMap**
│ (Private data, metadata, caches)
│
Need unique values only?
├── Need to iterate → **Set**
├── Just test membership → **Set** (for speed!) or **WeakSet** (if objects + GC)
│
Storing DOM-related data?
├── Need access later → **WeakMap** (element → data)
├── Just track membership → **WeakSet** (clicked elements, etc.)
Which one do you use most? Any creative use cases?
Follow @armorbreak for more JavaScript content.
Top comments (0)