Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ES2015+ ~22 min· topic 4 of 9

Topic 14.4

WeakMap, WeakSet and WeakRef

In one line

WeakMap and WeakSet hold objects without keeping them alive: when nothing else uses a key object, the garbage collector may remove it and its entry. WeakRef points at an object without keeping it alive. They exist for private data, caches and tagging objects you don't own.

Think of it like this

Sticky notes on library books. You stick a note on a book: "Asha borrowed this". When the library throws the book away, the note goes with it; you never have to remember to peel it off. A WeakMap is a pile of sticky notes attached to objects: the note lives exactly as long as the object does. A normal Map is a notebook that lists books: even after the library throws a book away, your notebook still mentions it, and the library can't recycle it because you're still "holding" it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Garbage collection (GC)
The engine automatically freeing memory used by objects the program can no longer reach.
Reachable
An object you can still get to from your variables, through any chain of properties or closures.
Strong reference
A normal reference (variable, property, Map key) that keeps an object alive.
Weak reference
A reference that doesn't keep the object alive. If only weak ones remain, the object can be freed.
WeakMap
A Map with object keys held weakly. Not iterable, no size.
WeakSet
A Set of objects held weakly. Answers "have I seen this object?".
WeakRef
A wrapper around one object that doesn't keep it alive. deref() gives the object or undefined.
FinalizationRegistry
Asks the engine to call a function some time after an object has been garbage collected.
Cycle
Objects that point to each other in a loop, like a.friend = b and b.friend = a.

Step by step

01See the leak a Map can cause

A Map key is a strong reference. When the element below is removed from the page, the Map still holds it, so the element and its data can never be freed. Over a long session with many such elements, memory keeps growing.

leak.jswhole filejavascript
const data = new Map();
const el = document.createElement("div");
data.set(el, { clicks: 0 });
el.remove(); // gone from the page...
// ...but data still holds el, so it stays in memory

02Swap in a WeakMap

Same code, one word changed. Once nothing else refers to the element, the GC can free it, and its entry vanishes with it. You never wrote any cleanup.

Swap in a WeakMapdiagram
Rendering diagram…

03Hide private data

Keep the WeakMap inside a module (Phase 12) so only your class can read it. Each instance is a key; its hidden state is the value.

04Remember visited objects

A WeakSet tracks what you've seen while walking a structure with cycles. Without it, a recursive walk of a.friend = b; b.friend = a would never end.

05Hold a cache entry weakly

A WeakRef lets a cache hand back an object if it's still around, and recompute it if not. Always handle the undefined case.

weak-cache.jswhole filejavascript
const cache = new Map(); // key -> WeakRef
function getImage(url) {
  const cached = cache.get(url)?.deref();
  if (cached) return cached;
  const img = decodeImage(url); // expensive
  cache.set(url, new WeakRef(img));
  return img;
}

Try it yourself

  1. 1

    Peek at the secret

    In the first example, try console.log(acc.balance = 999, acc.balance). Predict: there is only a getter, no setter. What does the second value show?

  2. 2

    Cache a copy

    In the second example, call stats([4, 9, 2]) with a new array literal instead of scores. Predict computeCount before running. Why does the cache miss?

  3. 3

    Use a Map instead

    Change new WeakMap() to new Map() in the first example. Which lines of output change, and what new thing can you now do with secrets?

Code & diagrams

WeakMap for private data ES2015+ New tab

The balance isn't a property of the object, so logging it doesn't show it. A WeakMap shows no contents and has no `size`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

150
Account { owner: 'Asha' }
true
WeakMap {}
size: undefined
TypeError: Invalid value used as weak map key
A per-object cache and a cycle-safe walk ES2015+ New tab

If `scores` were thrown away, its cache entry could be collected too. Without the WeakSet, `countObjects` would loop between `a` and `b` forever.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

{ total: 15, max: 9 }
{ total: 15, max: 9 }
computed 1 time(s)
set up now / already set up
2
WeakRef: deref() within one run ES2021+ New tab

The engine keeps a WeakRef's target alive until the current job ends, so these lines are predictable. After that, `deref()` may return `undefined` at any later time, or never; that part can't be shown with a fixed output.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

true
big data
still here in this run: true
function
FinalizationRegistry: cleanup some time after collection (not live: timing varies) ES2021+javascript
const registry = new FinalizationRegistry((key) => {
  // Runs some time after the object is collected, or maybe never.
  cache.delete(key);
});

function remember(key, value) {
  cache.set(key, new WeakRef(value));
  registry.register(value, key); // when value is collected, call back with key
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Loop over a WeakMap

Write const wm = new WeakMap(); for (const entry of wm) console.log(entry);

terminal
$ Console
── what you'll see ──
Uncaught TypeError: wm is not iterable

Break #2

Use a string as a WeakSet value

Write new WeakSet().add("asha");

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Invalid value used in weak set

Break #3

Base program logic on deref()

Build a feature that shows a message only when ref.deref() returns undefined.

terminal
$ the page shows
── what you'll see ──
The message appears after seconds in one browser, minutes in another, and never in a third.

Myth vs fact

Myth

A WeakMap's values are held weakly.

Fact

The keys are weak. A value stays alive as long as its key is alive (and as long as anything else refers to it).

Myth

Setting a variable to null frees the object at once.

Fact

It only removes one reference. The object is freed some time later, if nothing else reaches it, whenever the GC decides to run.

Myth

#private fields made WeakMaps useless.

Fact

Private fields cover private state in your own classes. WeakMaps still win for caches and metadata attached to objects you don't own, like DOM nodes or library objects.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    WeakMaps are built on ephemerons: an entry's value is only reachable if its key is reachable by some other path. This handles the case where the value refers back to its key (wm.set(obj, { owner: obj })): that cycle doesn't keep obj alive, unlike a Map.

  • ▸

    ES2023 allows non-registered symbols (from Symbol(), not Symbol.for()) as WeakMap keys, WeakSet values and WeakRef targets, because they are unique and can be collected. Registered symbols live forever, so they're rejected.

  • ▸

    WeakMaps cost more for the GC than Maps (ephemeron tables need extra marking passes). Use them where the weak semantics matter, not as a default dictionary.

  • ▸

    A FinalizationRegistry callback may never run (the page closes, or the engine skips it), and it runs as a separate task, never in the middle of your code. Don't use it to release critical resources such as file handles or locks; use explicit close() methods (or using declarations from the explicit resource management proposal, shipping in current Chrome).

Remember this

  1. 1

    To understand "weak", you need garbage collection (GC): the engine automatically frees memory used by objects that your program can no longer reach (Topic 14.8 explains how). An object is reachable if you can get to it from a variable, a property, a closure or anything else in use. A normal reference, like a variable or a Map key, is strong: it keeps the object reachable. A weak reference does not: if weak references are the only ones left, the object can be collected.

  2. 2

    A WeakMap is like a Map, with three rules. Keys must be objects (or, since ES2023, non-registered symbols); a string or number key throws TypeError: Invalid value used as weak map key. Keys are held weakly: the entry disappears when the key object is collected (the value is kept alive only as long as its key is). And it is not iterable and has **no size**: you can only set, get, has and delete with a key you already hold. That last rule is on purpose: if you could list the keys, the result would depend on when the GC happened to run.

  3. 3

    A WeakSet is the same idea for a Set: add, has, delete with objects only, no size, no loop. It answers "have I seen this object?" without keeping the object alive. Typical uses: marking DOM nodes you have already set up, or remembering visited objects while walking a structure that may have cycles (an object that points back to itself).

  4. 4

    Main uses of WeakMap: private data (store an object's hidden state in a WeakMap keyed by the object; outside code can't see it. Before #private fields arrived in ES2022, this was the standard way, see Topic 7.6), caches keyed by an object (results for a given array or DOM node, freed automatically when the object goes), and metadata on objects you don't own (adding a property to someone else's object can clash with theirs; a WeakMap entry can't).

  5. 5

    A WeakRef (ES2021) wraps one object weakly. ref.deref() returns the object if it is still alive, or undefined once it has been collected. Within one synchronous run of your code, an object you created or deref'd stays alive until that run finishes, so two deref() calls in a row agree. A FinalizationRegistry (ES2021) lets you ask for a callback some time after an object is collected, for example to remove its entry from a cache.

  6. 6

    Garbage collection timing is not something you can rely on. The GC may run soon, late or never (a short-lived page may exit first), and engines differ. So WeakRef and FinalizationRegistry are for optimisations like caches, never for program logic. The spec itself says: avoid them where possible. WeakMap and WeakSet are different: their behaviour you can observe (get, has) never depends on GC timing, which is why they're safe everyday tools.

Explain it without notes

01

What is the difference between a Map and a WeakMap, and why can't you iterate a WeakMap?

02

Give three real uses for WeakMap or WeakSet.

03

What does WeakRef.deref() return, and why shouldn't program logic depend on it?

04

Why must WeakMap keys be objects?

Practice

01

Write memoByObject(fn) that returns a function caching fn(obj) results in a WeakMap keyed by obj. Show that the second call with the same object doesn't call fn.

02

Write once(el, setup) that runs setup(el) only the first time it's called for a given element, using a WeakSet.

Trade-offs

  • ↔

    WeakMap entries clean themselves up and can't leak, but you can't list or count them, which makes debugging harder than with a Map you clear yourself.

  • ↔

    A WeakRef cache keeps memory low under pressure but may recompute values unpredictably; an LRU cache with a fixed size (a Map plus eviction) is predictable and testable.

Done when you can

  • I can explain strong vs weak references and reachability.

  • I can use a WeakMap for private data and per-object caches.

  • I can use a WeakSet to tag objects and to detect cycles.

  • I know why WeakMaps and WeakSets aren't iterable and accept only objects.

  • I can use WeakRef.deref() safely and know GC timing is unpredictable.