WeakMap and WeakSet
Learn how WeakMap and WeakSet work and when to use them for memory-efficient data storage.
WeakMap and WeakSet
WeakMap and WeakSet are the lesser-known siblings of Map and Set. They work similarly but with one fundamental difference — they hold weak references to their keys and values.
This means if the object you stored is no longer referenced anywhere else in your code, JavaScript's garbage collector can remove it automatically — even if it still exists in the WeakMap or WeakSet.
Understanding Weak References
First, a quick mental model of how memory works in JavaScript.
When you create an object and store it somewhere, JavaScript keeps it in memory as long as there is at least one reference to it. When nothing references it anymore, the garbage collector removes it.
let user = { name: "Ali" };
// user object is in memory — one reference
user = null;
// nothing references the object anymore
// garbage collector removes itA regular Map holds a strong reference — it counts as a reference and keeps the object alive even if you set the original variable to null.
let user = { name: "Ali" };
const map = new Map();
map.set(user, "some data");
user = null;
// The object is NOT garbage collected
// map still holds a strong reference to it
// map.size is still 1A WeakMap holds a weak reference — it does not count. If the object has no other references, it gets garbage collected regardless of being in the WeakMap.
let user = { name: "Ali" };
const weakMap = new WeakMap();
weakMap.set(user, "some data");
user = null;
// The object CAN now be garbage collected
// weakMap entry disappears automaticallyWeakMap
A WeakMap is like a Map with three important restrictions:
- Keys must be objects — not primitives
- It is not iterable — no
for...of, no.keys(), no.values() - No
.sizeproperty
These restrictions exist because WeakMap does not know what it contains at any given moment — entries can disappear at any time as the garbage collector runs.
const weakMap = new WeakMap();
const user1 = { name: "Ali" };
const user2 = { name: "Sara" };
weakMap.set(user1, { lastLogin: "2024-03-15" });
weakMap.set(user2, { lastLogin: "2024-03-10" });
console.log(weakMap.get(user1)); // { lastLogin: "2024-03-15" }
console.log(weakMap.has(user2)); // true
weakMap.delete(user1);
console.log(weakMap.has(user1)); // falseWeakMap Methods
Only four methods — no iteration:
weakMap.set(key, value); // add or update
weakMap.get(key); // retrieve value
weakMap.has(key); // check if key exists
weakMap.delete(key); // remove entryWhen to Use WeakMap
Storing private data for objects
Before private class fields (#field) existed, WeakMap was the standard way to attach private data to objects.
const _private = new WeakMap();
class User {
constructor(name, password) {
this.name = name;
_private.set(this, { password, loginAttempts: 0 });
}
checkPassword(input) {
const data = _private.get(this);
if (input === data.password) {
data.loginAttempts = 0;
return true;
}
data.loginAttempts++;
return false;
}
getLoginAttempts() {
return _private.get(this).loginAttempts;
}
}
const user = new User("Ali", "secret123");
console.log(user.checkPassword("wrong")); // false
console.log(user.checkPassword("wrong")); // false
console.log(user.getLoginAttempts()); // 2
console.log(user.checkPassword("secret123")); // true
console.log(user.getLoginAttempts()); // 0
// Private data is completely inaccessible from outside
console.log(user.password); // undefined
console.log(_private.get(user)); // only works if you have access to _privateWhen the user object is garbage collected, its entry in _private disappears automatically — no memory leak.
Caching computed values
const cache = new WeakMap();
function processElement(element) {
if (cache.has(element)) {
console.log("Using cached result");
return cache.get(element);
}
// Expensive computation
const result = {
width: element.offsetWidth,
height: element.offsetHeight,
area: element.offsetWidth * element.offsetHeight
};
cache.set(element, result);
return result;
}
const btn = document.querySelector("button");
processElement(btn); // computes and caches
processElement(btn); // returns cached
// When btn is removed from the DOM and no longer referenced
// the cache entry disappears automatically — no manual cleanup neededWith a regular Map, you would need to manually delete the entry when the element is removed. With WeakMap, it happens automatically.
Tracking metadata without preventing cleanup
const metadata = new WeakMap();
function attachHandler(element, handler) {
metadata.set(element, {
handler,
attachedAt: Date.now(),
clickCount: 0
});
element.addEventListener("click", () => {
const data = metadata.get(element);
data.clickCount++;
handler(data.clickCount);
});
}
const btn = document.querySelector("button");
attachHandler(btn, (count) => {
console.log(`Button clicked ${count} times`);
});
// When btn is removed from the DOM:
// - The WeakMap entry is garbage collected automatically
// - No memory leak from stale metadataWeakSet
A WeakSet is like a Set with the same weak reference behavior:
- Values must be objects — not primitives
- Not iterable — no
for...of, no.forEach() - No
.sizeproperty
const weakSet = new WeakSet();
const user1 = { name: "Ali" };
const user2 = { name: "Sara" };
weakSet.add(user1);
weakSet.add(user2);
console.log(weakSet.has(user1)); // true
console.log(weakSet.has(user2)); // true
weakSet.delete(user1);
console.log(weakSet.has(user1)); // falseWeakSet Methods
Only three methods:
weakSet.add(value); // add an object
weakSet.has(value); // check if object exists
weakSet.delete(value); // remove objectWhen to Use WeakSet
Tracking objects without preventing garbage collection
The most common use — marking objects as "processed", "visited", or "seen" without keeping them alive.
const processedOrders = new WeakSet();
function processOrder(order) {
if (processedOrders.has(order)) {
console.log(`Order ${order.id} already processed — skipping.`);
return;
}
// Process the order
console.log(`Processing order ${order.id}...`);
processedOrders.add(order);
}
const order1 = { id: 1, items: ["Laptop", "Mouse"] };
const order2 = { id: 2, items: ["Keyboard"] };
processOrder(order1); // Processing order 1...
processOrder(order1); // Order 1 already processed — skipping.
processOrder(order2); // Processing order 2...Preventing circular processing
const visiting = new WeakSet();
function processNode(node) {
if (visiting.has(node)) {
return; // already visiting this node — circular reference
}
visiting.add(node);
// Process node
console.log(node.value);
// Process children
for (const child of node.children ?? []) {
processNode(child);
}
visiting.delete(node);
}Tracking DOM elements
const clickedElements = new WeakSet();
document.querySelectorAll("button").forEach(btn => {
btn.addEventListener("click", () => {
if (clickedElements.has(btn)) {
console.log("Already clicked!");
return;
}
clickedElements.add(btn);
console.log("First click — doing something special");
btn.classList.add("clicked");
});
});
// When buttons are removed from the DOM
// they are automatically removed from clickedElements tooWeakMap vs WeakSet vs Map vs Set
// Map — strong reference, any key type, iterable, has .size
const map = new Map();
map.set("string key", value); // ✅ string keys allowed
map.set(objectKey, value); // ✅ object keys allowed
console.log(map.size); // ✅ size available
for (const [k, v] of map) {} // ✅ iterable
// WeakMap — weak reference, object keys only, not iterable
const weakMap = new WeakMap();
weakMap.set("string", value); // ❌ only object keys
weakMap.set(objectKey, value); // ✅
console.log(weakMap.size); // ❌ undefined
for (const [k, v] of weakMap) {} // ❌ not iterable
// Set — strong reference, any value, iterable, has .size
const set = new Set();
set.add(42); // ✅ primitives allowed
set.add(object); // ✅ objects allowed
console.log(set.size); // ✅
for (const v of set) {} // ✅ iterable
// WeakSet — weak reference, objects only, not iterable
const weakSet = new WeakSet();
weakSet.add(42); // ❌ only objects
weakSet.add(object); // ✅
console.log(weakSet.size); // ❌ undefined
for (const v of weakSet) {} // ❌ not iterableMap | WeakMap | Set | WeakSet | |
|---|---|---|---|---|
| Key/value types | Any | Objects only | Any | Objects only |
| References | Strong | Weak | Strong | Weak |
| Iterable | ✅ | ❌ | ✅ | ❌ |
.size | ✅ | ❌ | ✅ | ❌ |
| Auto-cleanup | ❌ | ✅ | ❌ | ✅ |
| Use when | General storage | Object metadata | Unique values | Object tracking |
When to Use Each
Need key-value storage?
├── Keys are objects AND want auto-cleanup → WeakMap
└── Everything else → Map
Need unique values?
├── Values are objects AND want auto-cleanup → WeakSet
└── Everything else → SetWeakMap and WeakSet are niche tools — you will not reach for them often. But when you need to attach data to objects without preventing garbage collection, they are exactly the right tool. The most common real-world use is caching, private data storage, and tracking DOM elements.
Summary
WeakMapandWeakSethold weak references — objects can be garbage collected even if they exist in them- Both only accept objects as keys or values — no primitives
- Both are not iterable — no
for...of, no.size, no.keys()or.values() - Entries disappear automatically when the object is garbage collected — no manual cleanup
- WeakMap — attach metadata or cached data to objects without memory leaks
- WeakSet — track which objects have been processed or visited without keeping them alive
- Use
MapandSetfor general purpose storage — useWeakMapandWeakSetonly when automatic cleanup from garbage collection is the specific requirement