If you've been using Maps and Sets in your JavaScript applications (if not, check out my previous tutorial), you might have come across their lesser-known cousins: WeakMap and WeakSet. These specialized collection types were also introduced in ES6, but they're often overlooked despite offering unique memory management benefits.
In this tutorial, I'll show you how WeakMaps and WeakSets work, what makes them "weak," and when you should use them instead of regular Maps and Sets. We'll explore practical use cases where these collections really shine.
What makes a collection "weak"?
The "weak" in WeakMap and WeakSet refers to how they handle references to objects. In a standard Map or Set, if you use an object as a key (for Maps) or as a value (for Sets), that collection maintains a strong reference to the object. This means that as long as the Map or Set exists, JavaScript's garbage collector can't remove those objects from memory - even if there are no other references to them in your code.
WeakMap and WeakSet, on the other hand, hold weak references to objects. This means that if the only remaining reference to an object is in a WeakMap or WeakSet, the garbage collector is free to remove it from memory.
This seemingly subtle difference has huge implications for memory management in certain scenarios, as we'll soon explore.
WeakMap: Garbage collector-friendly key-value pairs
A WeakMap works similarly to a Map, with these key differences:
- Keys must be objects (not primitives like strings or numbers)
- Keys are weakly referenced
- WeakMaps are not iterable (no loops or size property)
- WeakMaps can't be cleared all at once
Here's the basic syntax:
// Creating a WeakMap
const metadataMap = new WeakMap();
// Using objects as keys
const user1 = { id: 1, name: 'Alice' };
const user2 = { id: 2, name: 'Bob' };
// Setting values
metadataMap.set(user1, { lastLogin: '2024-01-15', visits: 10 });
metadataMap.set(user2, { lastLogin: '2024-02-20', visits: 5 });
// Getting values
console.log(metadataMap.get(user1)); // { lastLogin: '2024-01-15', visits: 10 }
// Checking if a key exists
console.log(metadataMap.has(user1)); // true
// Deleting an entry
metadataMap.delete(user1);
console.log(metadataMap.has(user1)); // false
The memory management advantage
The most important feature of WeakMaps is how they handle object references. Let's see it in action:
let user = { name: 'Ralph' };
// With a regular Map
const regularMap = new Map();
regularMap.set(user, 'metadata');
// With a WeakMap
const weakMap = new WeakMap();
weakMap.set(user, 'metadata');
// Now, let's remove our reference to the user object
user = null;
// In a regular Map, the user object is still in memory
console.log(regularMap.size); // 1 - object still referenced in the Map
// In a WeakMap, the entry will be automatically removed when GC runs
// No way to check WeakMap size, but the user object will be garbage collected
When we set user = null
, the only remaining reference to our original object is in the collections. The regular Map will keep that object in memory indefinitely, but the WeakMap allows it to be garbage collected because it only holds a weak reference.
Why no iteration methods?
You might have noticed that WeakMaps don't have methods like keys()
, values()
, entries()
, or even a size
property. WeakMaps cannot be iterated with for...of
loops either. This is directly related to the weak references feature.
Since object keys could be garbage collected at any time (and the garbage collector might run at unpredictable moments), it would be impossible to reliably iterate over a WeakMap or determine its size. If JavaScript allowed iteration of WeakMaps, you might start iterating, then halfway through, some keys could be garbage collected, leading to inconsistent results.
WeakSet: Collections of garbage-collectable objects
WeakSet is to Set what WeakMap is to Map. It holds a collection of unique objects, but with weak references to those objects. Here's the basic syntax:
// Creating a WeakSet
const seenObjects = new WeakSet();
// Adding objects
const obj1 = { id: 1 };
const obj2 = { id: 2 };
seenObjects.add(obj1);
seenObjects.add(obj2);
seenObjects.add(obj1); // Duplicate - will be ignored
// Checking if an object exists
console.log(seenObjects.has(obj1)); // true
// Deleting an object
seenObjects.delete(obj1);
console.log(seenObjects.has(obj1)); // false
Like WeakMap, WeakSet:
- Only accepts objects as values (not primitives)
- Holds weak references to those objects
- Is not iterable (no loops or size property)
- Doesn't have a clear() method
Practical use cases for WeakMap and WeakSet
These collections may seem limited at first, but they excel in specific scenarios:
1. Associating private data with DOM elements
WeakMaps are perfect for storing data that's associated with DOM elements, without causing memory leaks:
// Store private data associated with DOM elements
const elementData = new WeakMap();
document.querySelectorAll('.interactive-widget').forEach(element => {
// Associate data with each element
elementData.set(element, {
clickCount: 0,
initialState: element.innerHTML,
createdAt: new Date()
});
// Add event listeners
element.addEventListener('click', handleClick);
});
function handleClick(event) {
const element = event.currentTarget;
const data = elementData.get(element) || { clickCount: 0 };
// Update the associated data
data.clickCount++;
elementData.set(element, data);
console.log(`Element clicked ${data.clickCount} times`);
}
// The beauty here: when elements are removed from the DOM and
// garbage collected, their associated data in the WeakMap will
// also be eligible for garbage collection
If we had used a regular Map here, we'd have a memory leak—even after the DOM elements were removed, their data would remain in the Map.
2. Caching computed results for objects
WeakMaps are excellent for memoization when the input is an object:
// Cache for expensive calculations
const calculationCache = new WeakMap();
function calculateExpensiveResult(obj) {
// Check if we've already performed this calculation
if (calculationCache.has(obj)) {
console.log('Cache hit!');
return calculationCache.get(obj);
}
console.log('Cache miss, calculating...');
// Perform the expensive calculation
const result = performComplexComputation(obj);
// Cache the result
calculationCache.set(obj, result);
return result;
}
function performComplexComputation(obj) {
// Simulate an expensive operation
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += (obj.value * i) % 777;
}
return result;
}
// Usage
const data1 = { value: 42 };
const data2 = { value: 24 };
console.log(calculateExpensiveResult(data1)); // Cache miss
console.log(calculateExpensiveResult(data1)); // Cache hit
console.log(calculateExpensiveResult(data2)); // Cache miss
// If data1 is later garbage collected, its cached result will also be freed
With a regular Map, all cached results would remain indefinitely, potentially growing the memory footprint unnecessarily.
3. Marking objects that have been processed
WeakSets are perfect for marking objects as "seen" or "processed" without preventing them from being garbage collected:
// Keep track of processed objects
const processedObjects = new WeakSet();
function processObject(obj) {
// Skip if already processed
if (processedObjects.has(obj)) {
console.log('Already processed this object, skipping');
return;
}
console.log('Processing object:', obj);
// Do some heavy processing...
// Mark as processed
processedObjects.add(obj);
}
// Usage
const item1 = { id: 'a1', data: [1, 2, 3] };
const item2 = { id: 'a2', data: [4, 5, 6] };
processObject(item1); // Processing object: {id: 'a1', data: [1, 2, 3]}
processObject(item1); // Already processed this object, skipping
processObject(item2); // Processing object: {id: 'a2', data: [4, 5, 6]}
// When item1 and item2 are no longer referenced elsewhere,
// they'll be garbage collected along with their entries in the WeakSet
4. Implementing a non-leaking event listener registry
WeakMaps can help manage event listeners without causing memory leaks:
// Store listeners associated with objects
const listenerMap = new WeakMap();
function addListener(target, eventType, callback) {
// Get existing listeners for this target, or create a new entry
let listeners = listenerMap.get(target) || {};
// Initialize the array for this event type if needed
if (!listeners[eventType]) {
listeners[eventType] = [];
}
// Add the callback
listeners[eventType].push(callback);
// Update the WeakMap
listenerMap.set(target, listeners);
// Actually attach the listener
target.addEventListener(eventType, callback);
}
function removeAllListeners(target, eventType) {
const listeners = listenerMap.get(target);
if (listeners && listeners[eventType]) {
// Remove each attached listener
listeners[eventType].forEach(callback => {
target.removeEventListener(eventType, callback);
});
// Clear the registry
listeners[eventType] = [];
listenerMap.set(target, listeners);
}
}
// Usage
const button = document.querySelector('#my-button');
addListener(button, 'click', () => console.log('Clicked!'));
addListener(button, 'mouseover', () => console.log('Mouse over!'));
// Later, remove all click listeners
removeAllListeners(button, 'click');
// If the button is removed from the DOM, the listener registry
// entry in the WeakMap becomes eligible for garbage collection
5. Implementing a private class field
Private class fields aren't available right now (yet?), so WeakMaps are the only way to implement truly private data in classes:
// Private data storage for instances
const privateData = new WeakMap();
class User {
constructor(name, age) {
// Store private data
privateData.set(this, {
name,
age,
createdAt: new Date()
});
}
getName() {
return privateData.get(this).name;
}
getAge() {
return privateData.get(this).age;
}
// Only the class methods can access the private data
getCreationDate() {
return privateData.get(this).createdAt;
}
}
const user = new User('Leigh', 29);
console.log(user.getName()); // 'Leigh'
console.log(user.getCreationDate()); // Date object
// Can't access the data directly:
console.log(user.name); // undefined
console.log(privateData.get(user)); // { name: 'Leigh', age: 29, createdAt: Date }
// (But note that the WeakMap itself is still accessible if it's in scope)
When to use WeakMap and WeakSet
Use WeakMap or WeakSet when:
- Your keys/values are objects, not primitive values
- You want to associate metadata with objects without preventing garbage collection
- You need a cache that automatically cleans up entries for objects that are no longer used
- You want to mark objects as "processed" without keeping them in memory
- You're working with DOM elements and want to avoid memory leaks
Use regular Map or Set when:
- You need to store primitive values as keys (Map) or values (Set)
- You need to know the size of the collection
- You need to iterate over the collection (forEach, for...of)
- You need collection-wide operations like .clear()
Browser Support
WeakMap and WeakSet have excellent browser support, available in:
- Chrome 36+
- Firefox 34+
- Safari 8+
- Edge 12+
- IE 11+ (with some limitations)
For older browsers, you'll need a polyfill like the one provided by core-js:
import 'core-js/features/weak-map';
import 'core-js/features/weak-set';
Potential pitfalls
- Primitive values aren't allowed: Attempting to use primitive values with WeakMap or WeakSet will throw an error:
const weakMap = new WeakMap();
weakMap.set("string", 42); // TypeError: Invalid value used as weak map key
weakMap.set(42, "value"); // TypeError: Invalid value used as weak map key
const weakSet = new WeakSet();
weakSet.add("string"); // TypeError: Invalid value used in weak set
- No size information: You can't determine how many items are in a WeakMap or WeakSet:
const weakMap = new WeakMap();
weakMap.set({}, 'value');
// There's no size property or method to count entries
// console.log(weakMap.size); // undefined
- No iteration: You can't loop through entries:
const weakMap = new WeakMap();
weakMap.set({a: 1}, 'value1');
weakMap.set({b: 2}, 'value2');
// These methods don't exist:
// weakMap.keys()
// weakMap.values()
// weakMap.entries()
// weakMap.forEach()
Conclusion
WeakMap and WeakSet are specialized tools that fill a specific need in JavaScript: managing collections of objects while allowing unused objects to be garbage collected. When used in the right scenarios—like associating metadata with DOM elements, caching computed results, or tracking object processing—they help prevent memory leaks and improve the efficiency of your applications.
While they're more limited than their regular Map and Set counterparts (no iteration, no size property), their memory management benefits make them invaluable in certain situations.
Next time you're working with collections of objects and worried about memory management, consider reaching for WeakMap or WeakSet—they might be exactly the tool you need.