WeakMap and WeakSet
The garbage collection chapter left you with one rule: the engine keeps a value in memory as long as that value is reachable. Cut every reference to something and it becomes eligible for cleanup. WeakMap and WeakSet exist because sometimes you want to associate data with an object without being the thing that keeps it alive. To see why that matters, first watch how ordinary structures pin objects in memory.
Start with a lone object and a single reference to it:
let maya = { name: "Maya" };
// the object is reachable through maya
// drop the only reference
maya = null;
// nothing points here anymore, so the object can be collected
Reachability propagates through structures. Properties of an object, elements of an array, entries of a Map — while the container lives, everything it holds lives too, even if no other variable names them.
Put an object in an array and the array becomes its lifeline:
let maya = { name: "Maya" };
let array = [ maya ];
maya = null; // drop the variable reference
// the object still lives: the array holds it at array[0],
// so the garbage collector leaves it alone
{ name: “Maya” }
A regular Map works the same way. Use an object as a key and the Map keeps that object alive for as long as the Map itself exists:
let maya = { name: "Maya" };
let map = new Map();
map.set(maya, "...");
maya = null; // drop the variable reference
// the object survives as a key inside the map;
// you could still reach it through map.keys()
This is where WeakMap breaks the pattern. Its references to key objects are weak: they do not count toward reachability. If the only thing left pointing at an object is a WeakMap key, the garbage collector is free to reclaim it.
WeakMap
The first rule that separates WeakMap from a regular Map: keys must be objects. Primitives are rejected.
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ok"); // fine — the key is an object
// a string is a primitive, so this throws
weakMap.set("test", "Whoops"); // Error: Invalid value used as weak map key
Now the payoff. Store an object as a key, drop every other reference to it, and it leaves memory — and the WeakMap — on its own:
let maya = { name: "Maya" };
let weakMap = new WeakMap();
weakMap.set(maya, "...");
maya = null; // drop the last outside reference
// the object is gone from memory, and its entry is gone from weakMap
Compare this against the plain Map above. There, maya clung to life because the map’s key kept it reachable. Here, the WeakMap key doesn’t count, so once maya is null the object has nothing anchoring it.
Here’s the catch that shapes the whole API: WeakMap has no iteration and no keys(), values(), or entries(). You cannot list what’s inside it. The full method set is small:
Those four methods are the entire surface. Try them below — set a note on an object key, look it up, check membership, delete it, and watch what happens when you hand a string to a key that must be an object:
Why the restriction? It comes down to timing. When an object loses its last reference, it becomes collectible, but the specification doesn’t pin down when the engine actually reclaims it. The collector might run right away, or batch the work and sweep later once several objects have piled up. So at any given moment the true contents of a WeakMap are indeterminate — the engine may have removed an entry, or not yet, or be partway through. A reliable size or key list is impossible to offer, so the language simply doesn’t.
So what is a structure you can’t inspect actually good for?
Use case: additional data
The bread-and-butter use for WeakMap is side storage — data you want to attach to an object without owning that object’s lifetime.
Picture an object that belongs to someone else’s code, maybe a third-party library. You’d like to hang some data off it, but only for as long as the object itself is around. Drop your data into a WeakMap keyed by that object, and when the object is collected your data evaporates with it — no bookkeeping required.
weakMap.set(maya, "secret documents");
// when maya is collected, "secret documents" is discarded automatically
A concrete example: counting how many times each user visits. The user object is the key, the visit count is the value. When a user object goes away, you don’t want its stale count hanging around.
First, the naive version with a plain Map:
// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visit count
// bump the count for a user
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
And elsewhere, perhaps in another file, code that uses it:
// 📁 main.js
let maya = { name: "Maya" };
countUser(maya); // record a visit
// later, maya moves on
maya = null;
Now maya should be collectible, but it isn’t: visitsCountMap still holds it as a key. Unless you remember to delete that entry yourself, the map grows without bound as users come and go. In a large codebase, remembering to clean up every removed user everywhere becomes a genuine chore — and a memory leak when you forget.
Swap in a WeakMap and the problem dissolves:
// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visit count
// bump the count for a user
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
No manual cleanup. Once maya is unreachable everywhere except as a WeakMap key, the engine removes both the object and its count. The cleanup logic lives in the garbage collector, not in your application code.
The interaction below wires that up. Each button records a visit for one user by bumping a count kept in a WeakMap keyed by the user object — the count lives beside the user, not inside it:
Use case: caching
Caching follows the same shape. You compute a result for an object once, stash it, and hand back the stored copy on later calls. The trouble is knowing when to evict.
Here it is with a plain Map, which works but leaks:
// 📁 cache.js
let cache = new Map();
// compute and remember the result
function process(obj) {
if (!cache.has(obj)) {
let result = /* some expensive calculation on */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// used elsewhere:
// 📁 main.js
let obj = {/* say we have some object */};
let result1 = process(obj); // computed and cached
// ...later, from another spot in the code...
let result2 = process(obj); // served from cache, no recompute
// ...later still, obj is no longer needed:
obj = null;
alert(cache.size); // 1 — ouch, the object is still cached, holding memory
Repeated process(obj) calls compute once and read from cache after. But the cache keeps a strong reference, so even after obj = null the entry lingers. You’d have to evict it by hand.
Switch cache to a WeakMap and the eviction happens for you:
// 📁 cache.js
let cache = new WeakMap();
// compute and remember the result
function process(obj) {
if (!cache.has(obj)) {
let result = /* compute the result for */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// 📁 main.js
let obj = {/* some object */};
let result1 = process(obj);
let result2 = process(obj);
// ...later, obj is no longer needed:
obj = null;
// there's no cache.size to read on a WeakMap,
// but once obj is collected its cached result goes too
WeakSet
WeakSet is the same idea applied to Set:
- It’s like
Set, but only objects may be added — no primitives. - A member stays only while it’s reachable from somewhere outside the set.
- It supports
add,has, anddelete— but nosize, nokeys(), and no iteration.
Because it’s weak, a WeakSet also works as side storage. Not for arbitrary values, though — it stores a single bit of information per object: is this object in the set or not? Membership itself is the fact you’re recording.
Say you want to flag which users have visited:
let visitedSet = new WeakSet();
let maya = { name: "Maya" };
let raj = { name: "Raj" };
let lena = { name: "Lena" };
visitedSet.add(maya); // Maya visited
visitedSet.add(raj); // then Raj
visitedSet.add(maya); // Maya again — already a member, no duplicate
// visitedSet now holds 2 users
// did Maya visit?
alert(visitedSet.has(maya)); // true
// did Lena visit?
alert(visitedSet.has(lena)); // false
maya = null;
// Maya's entry is cleaned up automatically once he's unreachable
Toggle each user in and out of the set below. The WeakSet records nothing but a yes/no per object, and has() reads it straight back:
The defining limitation of both WeakMap and WeakSet is what you’ve now seen twice: no iteration, no way to read out the current contents. That can feel restrictive, but it doesn’t block the job they’re built for — being secondary storage for objects whose real home is somewhere else.
Sumlena
WeakMap is a Map-like collection whose keys must be objects. When a key object becomes unreachable everywhere else, the engine removes it along with its value.
WeakSet is a Set-like collection that stores only objects and drops them once they’re unreachable elsewhere.
Their strength is the weak reference: entries don’t stop the garbage collector, so they clean themselves up. The price is no clear, no size, no keys, no values, and no iteration.
Use them as secondary structures alongside your prilena object storage. The moment an object leaves the prilena store — leaving it referenced only as a WeakMap key or a WeakSet member — it gets swept away for you.