ES6 Note: Weakmap
The introduction and usage examples for Weakmap in ES6, with comparing to the Map.
The WeakMap
is a special type of collection in JavaScript. It was formally introduced in the ES6 standard and is similar to Map
, but with several key differences:
- A
WeakMap
is a collection of key-value pairs (key → value); - Keys can only be objects (plain objects, arrays, functions, etc.), not primitives (numbers, strings, booleans, etc.);
WeakMap
holds weak references to its keys, meaning: If no other variables reference the object anymore, the garbage collector will automatically reclaim it, and the corresponding entry in theWeakMap
will disappear.
Feature | Map | WeakMap |
---|---|---|
Key Types | Any value (objects or primitives) | Objects only |
Key Reference | Strong reference | Weak reference |
Enumerable | Enumerable (iterable via map.keys() , map.entries() ) | Not enumerable (no iteration methods) |
Use Cases | General key-value mapping storage | Data that needs to be tied to an object's lifecycle |
Why are weak references needed? Suppose we want to attach "extra information" to certain objects but don't want this information to prevent the objects from being garbage collected. For example:
const wm = new WeakMap();
function createUser(name) {
const user = { name };
wm.set(user, { created: Date.now() });
return user;
}
let alice = createUser("Alice");
console.log(wm.get(alice));
// { created: 1692624526805 } // e.g., stores creation time
// When alice is no longer referenced, the object will be GC'd, and the entry in the WeakMap disappears
alice = null;
Here, the role of WeakMap
is to store additional metadata without worrying about the object "living forever" because it's stored in a Map.
WeakMap
acts like an Object Additional Information Warehouse —the keys inside it do not affect object reclamation. Its greatest value is: following the object's lifecycle and avoiding memory leaks.
Many people mistakenly think that the "weak" reference in WeakMap is implemented via reference counting, but that's not the case.
Reference counting is a garbage collection algorithm: an object's reference count drops to zero → memory is released. However, modern JavaScript engines (V8, SpiderMonkey, etc.) do not rely solely on reference counting. Instead, they use garbage collection centered around reachability analysis. Therefore, the "weak reference" in WeakMap is not "delete when reference count = 0". Rather: The key in a WeakMap is not considered a "root of reachability".
In a regular Map
, the key object is strongly referenced. Even if no external variables reference it anymore, it remains "reachable" and won't be GC'd.
In a WeakMap
, the key is a weak reference. The garbage collector does not count it as part of the "live objects": if there are no other strong references from the outside, the key will be reclaimed by GC, and the corresponding key → value mapping will disappear.
Because the JS engine needs the freedom to remove entries during GC, if it allowed you to enumerate the WeakMap, you might see entries that "have been reclaimed but not yet cleaned up," making the semantics uncertain.
Therefore, WeakMap only supports set/get/has/delete
and cannot be iterated.
The underlying mechanism of WeakMap is not "reference counting" but is based on the reachability-based garbage collection mechanism. Its "weak reference" essentially tells the GC: Keys in the WeakMap do not count as active references. Thus, WeakMap is very suitable for storing "additional data that disappears along with the object."
Below is a diagram illustrating the difference in garbage collection between a regular Map and a WeakMap.
For a strongly referencing Map:
let map = new Map();
let obj = { name: "Alice" };
map.set(obj, "metadata");
// Now, even if obj = null,
// the map still "strongly references" this object
obj = null;
The memory relationship is:
[Global variable map] ──► [Map instance]
│
▼
┌───────────────┐
│ key: {Alice} │──► "metadata"
└───────────────┘
Even after obj = null
, the Map still holds {Alice}
, which won't be GC'd. This can easily cause memory leaks.
For a weakly referencing WeakMap:
let wm = new WeakMap();
let obj = { name: "Alice" };
wm.set(obj, "metadata");
// Remove the external reference to obj
obj = null;
Its memory relationship is as follows:
[Global variable wm] ──► [WeakMap instance]
│
▼
┌───────────────┐
│ key: {Alice} │──► "metadata"
└───────────────┘
After obj = null
, there are no strong references pointing to {Alice}
: GC considers {Alice}
"unreachable," reclaims the object, and the key-value pair in the WeakMap automatically disappears.
This does not prevent garbage collection.
In Node.js, directly observing garbage collection is difficult because GC is automatic.
However, starting from Node.js v14, WeakRef
and FinalizationRegistry
are available, allowing us to simulate the WeakMap reclamation process.
Here is some experimental code:
// Save a WeakMap
const wm = new WeakMap();
// Register a FinalizationRegistry to notify when an object is reclaimed
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object has been reclaimed, the corresponding metadata was: ${heldValue}`);
});
(function test() {
let obj = { name: "Alice" };
// Store a record in the WeakMap
wm.set(obj, "metadata for Alice");
// Register a reclamation callback for the object
registry.register(obj, "metadata for Alice");
console.log("Object created and stored in WeakMap");
// Manually clear the strong reference to obj
obj = null;
})();
// Prompt Node.js to perform garbage collection
// ⚠️ Requires running with the flag: node --expose-gc test.js
setTimeout(() => {
console.log("Attempting to manually trigger garbage collection");
if (global.gc) {
global.gc();
} else {
console.log("Please run this script with node --expose-gc");
}
}, 1000);
Next, save the code as test.js
and run it with the GC flag:
node --expose-gc test.js
You will see output similar to:
Object created and stored in WeakMap
Attempting to manually trigger garbage collection
Object has been reclaimed, the corresponding metadata was: metadata for Alice
Thus, we can see:
- The key in
WeakMap
does not prevent object reclamation; FinalizationRegistry
can trigger a callback after the object is GC'd, proving the WeakMap entry has disappeared;- This shows the essence of WeakMap's "weak reference" is: It does not treat the key as a root of reachability for GC.
Below are several typical use cases for WeakMap. You'll find it's primarily used for "attaching extra information to objects without wanting to affect the object's lifecycle":
- Private properties (security + automatic release)
- Caching (automatically cleaned up when the object is released)
- DOM/object metadata storage (without polluting the original object)
- Event listener management
- Implicit information attachment inside libraries
Private property storage (simulating class private fields)
const privateData = new WeakMap();
class User {
constructor(name, password) {
this.name = name;
privateData.set(this, { password });
}
checkPassword(pw) {
return privateData.get(this).password === pw;
}
}
const u = new User("Alice", "123456");
console.log(u.checkPassword("123456")); // true
console.log(u.password); // undefined (cannot access directly)
Using WeakMap to save "private data"; when the object instance is reclaimed, the data is automatically released too.
Caching (Memoization)
const cache = new WeakMap();
function heavyCompute(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
let result = expensiveCalculation(obj);
cache.set(obj, result);
return result;
}
function expensiveCalculation(o) {
return JSON.stringify(o).length; // Assume it's computationally expensive
}
let data = { foo: "bar" };
console.log(heavyCompute(data)); // Compute and cache
console.log(heavyCompute(data)); // Read directly from cache
data = null; // Release the object, cache automatically cleans up
Ideal for "object → result" caching scenarios, prevents memory leaks.
DOM metadata storage
const elementData = new WeakMap();
function bindInfo(el, info) {
elementData.set(el, info);
}
function getInfo(el) {
return elementData.get(el);
}
// Usage
const button = document.querySelector("button");
bindInfo(button, { clicked: 0 });
button.addEventListener("click", () => {
const info = getInfo(button);
info.clicked++;
console.log(`Button click count: ${info.clicked}`);
});
When the button
node is removed from the DOM and becomes unreachable, the WeakMap automatically clears the metadata, preventing memory leaks.
Event listener state tracking
const listenerMap = new WeakMap();
function trackListener(obj, listener) {
obj.addEventListener("click", listener);
listenerMap.set(obj, listener);
}
function removeListener(obj) {
const listener = listenerMap.get(obj);
obj.removeEventListener("click", listener);
listenerMap.delete(obj);
}
Tracks bound listeners and automatically releases them.
Cross-library/cross-module additional information
If you are writing a library and don't want to pollute user objects (avoid adding properties directly), WeakMap can "attach" data externally:
// myLib.js
const meta = new WeakMap();
export function attach(obj, info) {
meta.set(obj, info);
}
export function getInfo(obj) {
return meta.get(obj);
}
Users won't see extra properties, and there's no risk of memory leaks.