JavaScript Map, Set, WeakMap, and WeakSet: When to Use Which
Stop using plain objects for everything. These built-ins have superpowers.
Map vs Object
// Object — the old way
const obj = {
name: 'Alex',
age: 30,
};
obj['name']; // "Alex"
obj.email = 'a@b.com'; // Add
delete obj.age; // Remove
Object.keys(obj); // ['name', 'email']
Object.values(obj); // ['a@b.com']
// Map — the better way for many cases
const map = new Map();
map.set('name', 'Alex'); // Set value
map.set('age', 30);
map.set(42, 'answer'); // Key can be ANY type (not just string!)
map.set({}, 'object key'); // Even objects as keys!
map.get('name'); // "Alex"
map.has('age'); // true
map.delete('age'); // true
map.size; // 2 (no need to manually count!)
// Iteration is easy
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// Insertion order preserved!
const orderedMap = new Map();
orderedMap.set('z', 1);
orderedMap.set('a', 2);
orderedMap.set('m', 3);
[...orderedMap.keys()]; // ['z', 'a', 'm'] — insertion order!
// Object doesn't guarantee order (well, it does now in practice, but Map was designed for it)
When to Use Map vs Object
| Use Case | Use |
|---|---|
| String keys only, simple data | Object |
| Need to know size |
Map (size property) |
| Non-string keys | Map |
| Frequent add/remove | Map |
| JSON serialization needed |
Object (Map can't be JSON.stringified directly) |
| Need insertion order | Map |
| Prototypes matter (hasOwnProperty issues) | Map |
Set — Unique Values Only
// Array with duplicates
const numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5];
// Create a set (removes duplicates!)
const uniqueSet = new Set(numbers);
uniqueSet; // Set { 1, 2, 3, 4, 5 }
uniqueSet.size; // 5
// Convert back to array
[...uniqueSet]; // [1, 2, 3, 4, 5]
Array.from(uniqueSet); // Same thing
// Common use cases:
const arr1 = [1, 2, 3];
const arr2 = [2, 3, 4];
const union = new Set([...arr1, ...arr2]); // {1,2,3,4}
const intersection = new Set(arr1.filter(x => arr2.includes(x))); // {2,3}
const difference = new Set(arr1.filter(x => !arr2.includes(x))); // {1}
// Set methods
const set = new Set([1, 2, 3]);
set.add(4); // Add
set.has(2); // true (exists?)
set.delete(3); // true (remove)
set.clear(); // Remove all
set.forEach(x => console.log(x)); // Iterate
// Practical: Track unique visitors
const pageViews = new Set();
function trackVisit(userId) {
if (!pageViews.has(userId)) {
console.log(`New unique visitor! Total: ${pageViews.size + 1}`);
}
pageViews.add(userId);
}
trackVisit('user_1'); // New unique visitor! Total: 1
trackVisit('user_2'); // New unique visitor! Total: 2
trackVisit('user_1'); // Already visited (no log)
WeakMap — Memory-Friendly Object Metadata
// Problem with regular Map/Objects:
let user = { name: 'Alex' };
const metadata = new Map();
metadata.set(user, { loginCount: 0, lastLogin: null });
user = null; // We're done with user...
// But metadata still holds a reference! → MEMORY LEAK!
// Solution: WeakMap (keys are garbage collected when no other reference exists)
let user2 = { name: 'Sam' };
const weakMetadata = new WeakMap();
weakMetadata.set(user2, { loginCount: 0 });
user2 = null;
// weakMetadata's entry is now eligible for GC!
// No memory leak!
// Practical use case: Private class fields (before # was available)
class User {
#data = new WeakMap();
constructor(name) {
this.#data.set(this, { name, createdAt: Date.now() });
}
get name() { return this.#data.get(this).name; }
}
// Another practical use: DOM element metadata
const elementData = new WeakMap();
function attachData(el, data) {
elementData.set(el, data);
}
function getData(el) {
return elementData.get(el); // Returns undefined if el removed from DOM
}
WeakSet — Tracking Without Preventing GC
// Similar to WeakMap but for values (not key-value pairs)
// Useful for: "Is this object in my collection?" without preventing GC
const activeElements = new WeakSet();
function activate(element) {
activeElements.add(element);
element.classList.add('active');
}
function deactivate(element) {
activeElements.delete(element);
element.classList.remove('active');
}
function isActive(element) {
return activeElements.has(element);
}
// When elements are removed from DOM, they're automatically removed from the WeakSet
// No manual cleanup needed!
// Practical: Instance checking
const instances = new WeakSet();
class Singleton {
constructor() {
if (instances.has(Singleton)) {
throw new Error('Singleton already exists!');
}
instances.add(Singleton);
}
}
Performance Comparison
// For frequent lookups on large datasets:
// Object: Good for string keys, widely supported
// Map: Faster for frequent add/delete, any key type
// Set: O(1) lookups for uniqueness checks
// WeakMap/WeakSet: Slightly slower but prevents memory leaks
// Benchmark example:
const N = 100000;
// Object lookup
const obj = {};
for (let i = 0; i < N; i++) obj[`key_${i}`] = i;
console.time('Object lookup');
for (let i = 0; i < N; i++) obj[`key_${i}`];
console.timeEnd('Object lookup');
// Map lookup
const map = new Map();
for (let i = 0; i < N; i++) map.set(`key_${i}`, i);
console.time('Map lookup');
for (let i = 0; i < N; i++) map.get(`key_${i}`);
console.timeEnd('Map lookup');
// Set has check
const set = new Set();
for (let i = 0; i < N; i++) set.add(i);
console.time('Set has');
for (let i = 0; i < N; i++) set.has(i);
console.timeEnd('Set has');
Quick Reference
| Feature | Object | Map | Set | WeakMap | WeakSet |
|---|---|---|---|---|---|
| Key type | string/symbol | Any | Value (any) | Object only | Object only |
| Size | Manual (keys().length) |
.size |
.size |
No (not enumerable) | No |
| Order | Yes (insertion) | Yes (insertion) | Yes (insertion) | No | No |
| Iteration |
for...in, Object.entries()
|
for...of |
for...of |
No | No |
| Serialization | JSON.stringify |
Manual | Manual | No | No |
| GC safe | No | No | No | Yes | Yes |
| Best for | Simple data, JSON | Dynamic collections | Uniqueness | Private data | Membership tracking |
Which one do you use most? Any creative use cases?
Follow @armorbreak for more JavaScript content.
Top comments (0)