WeakMap & WeakSet
Attach data to objects without keeping them alive: use WeakMap for per-object caches and private data, WeakSet for tracking objects, and non-registered symbols as modern weak keys.
- 01Explain what weak meansSay why a WeakMap pair does not keep its key object alive.
- 02Use the WeakMap and WeakSet methodsUse
set,get,has, anddeletewith keys that are allowed, and know which keys throw aTypeError. - 03Choose the right collectionPick Map, WeakMap, Set, or WeakSet for a cache, notes on an object, or marking objects you have seen.
Notes that belong to an object
You have already met Map. A Map stores pairs: each key leads to one value. You can count its pairs, loop over them, and keep them as long as you like.
A WeakMap also stores pairs, but each key must be an object. It is made for one job: attach extra data to an object without keeping that object alive.
What does "keeping an object alive" mean? JavaScript frees the memory of objects that your code can no longer get to. This cleanup is called garbage collection. A normal Map is one way to get to its keys, so a Map stops its keys from being cleaned up. A WeakMap does not.
You will use this idea in three ways in this lesson:
- Saving a result for each object, so you don't work it out twice. This is called a cache.
- Keeping private data for each object.
- Marking objects you have already handled, with a
WeakSet.
A WeakMap stores values by key. Each key must be an object, or a symbol made with Symbol(). The WeakMap holds its keys weakly: if nothing else in your code can reach a key, its pair may disappear. A WeakSet remembers which objects it holds, and holds them weakly in the same way. Only the link to the key is weak. A stored value is held like any normal value.
Picture a party. Each guest wears a name tag with a note on it. A guest list keeps every name, even after a guest goes home. A name tag is different: when the guest leaves, the tag leaves too. A Map is like the guest list. A WeakMap is like the name tags.
- In real life: A guest wearing a name tag
- In JavaScript: An object used as a key
- In real life: A guest list keeps every name, even after guests go home
- In JavaScript: A
Mapkeeps its keys - In real life: The name tag leaves with the guest
- In JavaScript: A
WeakMappair can go when its key object goes - In real life: Nobody can read a list of old name tags
- In JavaScript: A
WeakMaphas nosize, nokeys(), and no loop
This lesson uses ideas from Map and Set, Symbols, Objects & references, and Closures. Two later lessons, Memory management & garbage collection and Weak references & ephemerons, go deeper. Here you need one word. An object is reachable when your code can still get to it through a variable or through another object.
Keys a WeakMap does not keep
STEP THROUGHThe "weak" in WeakMap does not mean small or slow. It means the WeakMap's link to each key is weak. That link alone does not keep the key alive.
While your code can still reach the key in some other way, a WeakMap works like a Map. get, has, set, and delete all work as usual. The difference only shows when the rest of your code lets go of the key.
Step through one object used as a key in both a Map and a WeakMap:
Step through one object used as a key in a Map and in a WeakMap. While the object is still reachable, get gives the same kind of answer in both.
script
const strong = new Map();const weak = new WeakMap(); strong.set(profile, "kept by Map");weak.set(profile, "attached weakly"); console.log(strong.get(profile));console.log(weak.get(profile));The replay never shows a pair disappearing. That is on purpose. Your code can't see when garbage collection runs. The engine, the program that runs your JavaScript (like the one inside Chrome), picks the time and keeps it private. This lesson can show you the rules, and a model of which objects are reachable. It can't show the engine's real memory.
const firstUser = { id: 7 };const secondUser = { id: 7 };const notes = new WeakMap(); notes.set(firstUser, "first");console.log(notes.get(firstUser));console.log(notes.has(secondUser));Lines 1 and 2 make two different objects. Both happen to hold id: 7. Line 5 stores a note for firstUser, and line 6 prints first. Line 7 asks about secondUser and prints false.
id: 7, but they are two objects. The note was stored for firstUser, so only that object has it.A WeakMap finds a key by which object it is, not by what the object holds. Two objects with the same contents are still two different keys.
The methods and the key rules
REAL CODEA WeakMap has only four methods:
set(key, value)stores a value for a key.get(key)reads the value back.has(key)answerstrueorfalse: is there a pair for this key?delete(key)removes the pair.
A WeakSet has three, the same ones a Set uses to add and check items: add, has, and delete.
The rule for keys is strict:
- A WeakMap key must be an object, or a symbol made with
Symbol(). - A WeakSet item follows the same rule.
- Anything else, like a string or a number, throws a
TypeError.
Symbol() can be WeakMap keys and WeakSet items. Strings, numbers, and symbols from Symbol.for() throw a TypeError.A WeakMap holds its stored values in the normal way. If a value points to another object, that object stays reachable through the value. Only the WeakMap's link to the key is weak.
const metadata = new WeakMap();const user = {};const settings = { theme: "light" }; metadata.set(user, settings);console.log(metadata.get(user).theme);Line 3 makes a settings object. Line 5 stores it as the value for user. Line 6 prints light. The value is held normally, so it is still there.
Now run all the rules in one program. Watch for two errors at the end:
Run the rules in real code. The object key and the Symbol() key work. Then come two TypeErrors.
script
const weakSet = new WeakSet();const obj = {};const localSymbol = Symbol("token");const registered = Symbol.for("token"); weakMap.set(obj, "object ok");weakSet.add(obj);weakMap.set(localSymbol, "symbol ok"); console.log(weakMap.get(obj));console.log(weakSet.has(obj));console.log(weakMap.get(localSymbol));console.log(typeof weakMap.size);console.log(typeof weakMap.keys); try { weakMap.set("id", 1); } catch (error) { console.log(error.name); }try { weakMap.set(registered, 1); } catch (error) { console.log(error.name); }const notes = new WeakMap();const user = {}; notes.set(user, "saved");console.log(notes.delete(user));console.log(notes.has(user));console.log(notes.delete(user));Line 5 removes the pair and prints true. Line 6 checks the same object and prints false: the pair is gone. Line 7 tries again. There is nothing left to remove, so it prints false.
Here your own code removed the pair, so you know exactly when it happened. Garbage collection is different: the engine picks the time, and your code can't see it.
const notes = new WeakMap();const user = {}; console.log(notes.get(user));console.log(notes.has(user));Asking about a key that has no pair is safe. Line 4 prints undefined. Line 5 prints false.
const notes = new WeakMap();const user = {}; notes.set(user, undefined);console.log(notes.get(user));console.log(notes.has(user));Sometimes the stored value is undefined on purpose. Line 4 stores it. Line 5 prints undefined, the same as for a missing key. So get alone can't tell you if the pair exists. Line 6 prints true: has answers that question.
The table lists each weak collection: what it accepts, which methods it has, and which Map or Set features it leaves out.
| Collection | What it accepts | Methods it has | What it does not have |
|---|---|---|---|
WeakMap | Objects, and symbols made with Symbol(), as keys. | set, get, has, delete | No size, keys, values, entries, or forEach. |
WeakSet | Objects, and symbols made with Symbol(), as items. | add, has, delete | No size, no loop, and no forEach. |
Imagine a WeakMap had a size. You could check it again and again. The moment it dropped, you would know the engine had just cleaned up an object. JavaScript keeps that timing private. So WeakMap and WeakSet have no size, and you can't loop over them.
When an object can be cleaned up
INTERACTIVEThink of links as arrows. Many things can point to an object:
- a variable
- a property of another object
- an item in an array
- a function that remembers it (a closure)
- a key in a normal Map
If your running code can follow some chain of these arrows to an object, the object is reachable. When no chain is left, the engine is allowed to clean the object up. A WeakMap's arrow to its key doesn't count.
user = null, a Map still holds the object as a key, so the object stays. A WeakMap's link does not count, so nothing holds the object. The engine may clean it up later.Try it. Pick Map or WeakMap, then run user = null and see which arrows are left:
let user = { name: "Ada" };const notes = new WeakMap();notes.set(user, "cached stats"); user = null; // drop the last variable referenceuser→ the object{ name: "Ada" }your code can reach itThe variable user points to the object, so a lookup works in a Map or a WeakMap. This is a model of the arrows. It does not look at real memory.
The model is drawn with cards. It does not look at real memory. "Can be cleaned up" means the engine may free the object at some later time. It does not mean that has happened yet, and your code gets no signal when it does.
Saving a result for each object
STEP THROUGHA cache saves the result of slow work, so the next call can reuse it. When the input is an object, a WeakMap makes a good cache:
- The saved result belongs to that exact object.
- The cache does not keep old objects alive forever.
Step through a cache that saves stats for a user object:
Step through a cache that saves a result for each object. The first call does the work. The second call reuses it. The cache never keeps the user alive by itself.
script
let calculations = 0; function userStats(user) { if (statsCache.has(user)) return statsCache.get(user); calculations = calculations + 1; const stats = { label: user.name, score: user.visits * 10 }; statsCache.set(user, stats); return stats;} const ada = { name: "Ada", visits: 3 };console.log(userStats(ada).score);console.log(userStats(ada).score);console.log(calculations);You will find this pattern in real code, for example when you save:
- totals worked out from user records
- facts about the parts of a code tree
- the measured size of elements on a page
While some part of your app still uses the object, the cache works. Once nothing uses the object, its cache pair can be cleaned up with it.
- You need to list every key later
- Save measured sizes for page elements that other code created
- Count how often each word appears
- Attach check results to user objects from another library
- Show how many players are in the table right now
- Store hidden data for class objects in older code
Pick Map when you need to loop, count, or use strings and numbers as keys. Pick WeakMap for extra data that should last only as long as an object does.
Private data
COMPAREA class can keep its data in a WeakMap instead of on the object. Before JavaScript had #private fields, many libraries did this:
- Put one WeakMap outside the class.
- Use each new object,
this, as a key. - Store the hidden data as the value.
Code outside can't read the WeakMap unless it can reach the variable that holds it.
const privateData = new WeakMap(); class Counter { constructor(name) { privateData.set(this, { name, count: 0 }); } click() { const data = privateData.get(this); data.count = data.count + 1; return data.name + ": " + data.count; }} const counter = new Counter("save");console.log(counter.click());console.log(counter.count);Line 1 makes the WeakMap. Line 5 stores { name, count: 0 } with the new object as the key. Lines 8 and 9 get that data back and add 1 to the count. Line 15 prints save: 1. Line 16 prints undefined, because count is not a property of counter. It lives in the WeakMap.
The table compares three ways to keep data private. Each row says what the way is good at and what to watch for.
| Way to hide data | What it is good at | What to watch for |
|---|---|---|
| A closure | A function returns other functions, and they remember hidden variables. | Great for functions. Each object may get its own copy of every method. |
| A WeakMap | Objects made from a class, in code written before #private fields. | You need a WeakMap outside the class, and you must use this with care. |
#private fields | Modern class code. The language itself blocks access from outside. | They only work inside a class. The Private fields & methods lesson covers them. |
Compare this with the Closures lesson. A closure hides variables because a function remembers the variables around it. The WeakMap way hides a lookup table instead. In modern classes, #private fields are usually easier to read.
Notes on objects you do not own
PRACTICALSometimes a library hands you an object. You should not add your own fields to it, because the library owns it. A WeakMap lets you keep your notes beside the object, without changing the object.
const validation = new WeakMap();const form = { email: "asha@example.com" }; validation.set(form, { valid: true });console.log(validation.get(form).valid);console.log(form.valid);Line 2 is a form object from another part of the app. Line 4 stores a check result for it. Line 5 reads the result from the WeakMap and prints true. Line 6 prints undefined, because the form itself did not change. When the rest of the app lets go of the form, the form can still be cleaned up.
Use a WeakMap for notes or a cache that should last only as long as an object. Use a normal Map when your code owns the data, needs to count it, or must list it in a report. A WeakMap does not replace every Map.
WeakSet: marking objects you have seen
INTERACTIVEA WeakSet answers one question: "Have I seen this object?" It stores no values. Use it for objects you handle once, like:
- widgets on a page that you have already set up
- points you have already visited while walking through a tree or a graph
- page elements that some other code created
A WeakSet holds its objects weakly, so it is never the reason an object stays alive. To pick the right tool:
- Need a saved result for each object? Use a WeakMap.
- Need to list or count the items? Use a normal Set.
const seen = new WeakSet();const card = {}; seen.add(card);console.log(seen.has(card)); try { seen.add("card"); }catch (error) { console.log(error.name); }Line 4 adds the object from line 2, and line 5 prints true. Line 7 tries to add the string "card". A string is not an object, so it throws, and line 8 prints TypeError. A WeakSet follows the same rule as WeakMap keys: objects, and symbols made with Symbol().
const seen = new WeakSet();const card = {}; seen.add(card);console.log(seen.delete(card));console.log(seen.has(card));Line 5 removes the card and prints true. Line 6 prints false. So you can take an object out of a WeakSet yourself, even though you can't list what is inside.
const processed = new WeakSet();const firstTodo = {};const secondTodo = {}; processed.add(firstTodo);console.log(processed.has(firstTodo));console.log(processed.has(secondTodo));Lines 2 and 3 make two separate todo objects. Line 5 adds only the first one. Line 6 prints true for it. Line 7 prints false for the second. Like a WeakMap, a WeakSet checks which object it is, not what the object holds.
At a concert, staff stamp your hand when you come in. If you step out and come back, they check the stamp and let you through. When you go home, the stamp goes home with you. A WeakSet marks objects in the same way.
- In real life: The guest
- In JavaScript: An object
- In real life: The stamp on the hand
- In JavaScript: Being in the WeakSet
- In real life: The door staff check for the stamp
- In JavaScript:
weakSet.has(object)
const processed = new WeakSet(); function initialize(card) { if (processed.has(card)) return "already initialized"; processed.add(card); return "initialized now";} const card = { id: "signup" };initialize(card);initialize(card);Not run yet
The WeakSet can answer has(card), but it can't list the cards it holds.
Press the button. The first call will add the object to the WeakSet.
card in the WeakSet, so it adds it and does the work. The second call finds it already there and skips the work.Symbols as WeakMap keys
ES2023Since 2023, JavaScript also lets you use some symbols as WeakMap keys. A symbol is a unique value. There are two ways to make one:
Symbol("local")makes a brand-new symbol. When no code holds it any more, it can be cleaned up. So it can be a WeakMap key.Symbol.for("name")gets a symbol from one shared list for the whole program, called the global symbol registry. That list keeps the symbol forever. So this kind, called a registered symbol, can't be a WeakMap key.
const local = Symbol("local");const shared = Symbol.for("shared");const metadata = new WeakMap(); metadata.set(local, "ok");console.log(metadata.get(local)); try { metadata.set(shared, "nope"); }catch (error) { console.log(error.name); }Line 1 makes a local symbol. Line 5 uses it as a key, and line 6 prints ok. Line 2 makes a registered symbol. Line 8 tries to use it as a key. That throws, and line 9 prints TypeError. The Symbols lesson explains both kinds of symbol in more detail.
Common misconceptions
- "WeakMap is just a Map with fewer methods." The missing methods are on purpose. Without them, your code can't find out when the engine cleans up an object.
- "I can make a WeakMap pair disappear right now by dropping the key." You can let go of every link to the key, but the engine still decides when to clean up. To remove a pair right now, call
delete. - "Any value can be a WeakMap key." Strings, numbers, booleans,
null, andundefinedthrow aTypeError. So does a symbol fromSymbol.for(). - "A WeakSet is for unique strings and numbers." That is a Set's job. A WeakSet only holds objects, and symbols made with
Symbol(). - "Private data should always use a WeakMap." In modern class code,
#privatefields are usually the better choice.
Practice exercises
5 EXERCISESRun or read the code. Which error name does it print?
const wm = new WeakMap();
try { wm.set("id", 1); }
catch (error) { console.log(error.name); }const wm = new WeakMap();
try { wm.set("id", 1); }
catch (error) { console.log(error.name); }"id" is a string, not an object. So wm.set throws a TypeError, and the catch line prints its name.
The function is called twice. Why does the counter not reach 2?
const cache = new WeakMap();
let calls = 0;
function score(user) {
if (cache.has(user)) return cache.get(user);
calls = calls + 1;
const result = user.visits * 10;
cache.set(user, result);
return result;
}
const ada = { visits: 4 };
console.log(score(ada));
console.log(score(ada));
console.log(calls);const cache = new WeakMap();
let calls = 0;
function score(user) {
if (cache.has(user)) return cache.get(user);
calls = calls + 1;
const result = user.visits * 10;
cache.set(user, result);
return result;
}
const ada = { visits: 4 };
console.log(score(ada));
console.log(score(ada));
console.log(calls);The first call works out 40 and stores it. The second call finds 40 in the WeakMap and returns it before calls goes up. So the last line prints 1.
Which two words does the code print?
const processed = new WeakSet();
function init(item) {
if (processed.has(item)) return "skip";
processed.add(item);
return "init";
}
const card = {};
console.log(init(card));
console.log(init(card));const processed = new WeakSet();
function init(item) {
if (processed.has(item)) return "skip";
processed.add(item);
return "init";
}
const card = {};
console.log(init(card));
console.log(init(card));The first call adds the object and returns init. The second call finds the same object already in the WeakSet and returns skip.
Fill in the missing word.
A WeakMap has no size. The number of keys could drop at any moment when the engine cleans up objects. If you could read that number, you could tell exactly when the cleanup ran.
You need to count how many times each word appears in some text, then print a report. Which collection should you pick?
Use Map. A WeakMap can't take string keys, and it can't count or list its pairs.
Check your understanding
7 QUESTIONSQuestion 1 of 7What is weak about a WeakMap key?
Choose an answer to see the explanation.
Question 2 of 7What does the WeakMap lookup print?
Read the code, then predictconst wm = new WeakMap(); const key = {}; wm.set(key, "ok"); console.log(wm.get(key));Choose an answer to see the explanation.
Question 3 of 7What does this code print?
Read the code, then predictconst wm = new WeakMap(); try { wm.set("id", 1); } catch (error) { console.log(error.name); }Choose an answer to see the explanation.
Question 4 of 7Why does a WeakMap have no
sizeand nokeys()?Choose an answer to see the explanation.
Question 5 of 7What does this WeakSet code print?
Read the code, then predictconst seen = new WeakSet(); const card = {}; seen.add(card); console.log(seen.has(card)); console.log(typeof seen.size);Choose an answer to see the explanation.
Question 6 of 7Which symbol can be a WeakMap key in modern JavaScript?
Choose an answer to see the explanation.
Question 7 of 7You want a cache keyed by user objects. It must not keep old users alive. Which do you pick?
Choose an answer to see the explanation.
Key takeaways
- A WeakMap holds its keys weakly: a pair does not keep its key object alive.
- Use a WeakMap to store a value for each object. Use a WeakSet to mark objects you have seen.
- There is no
size, nokeys(), and no loop, so your code can't tell when cleanup happens. - Symbols made with
Symbol()can be WeakMap keys. Symbols fromSymbol.for()can't. - For private data in a modern class, use
#privatefields unless you need the WeakMap way.
Final definition: WeakMap and WeakSet attach information to objects without keeping those objects alive.
Up next: JSON.