WeakSet & WeakMap
Introduction to Weak Collections
JavaScript provides two specialized collection types -- WeakSet and WeakMap -- that work similarly to Set and Map but with one critical difference: they hold weak references to their keys. A weak reference means the JavaScript engine's garbage collector can reclaim the memory of an object stored in a weak collection if there are no other references to that object anywhere in your program. This behavior makes weak collections ideal for scenarios where you need to associate metadata with objects without preventing those objects from being cleaned up when they are no longer needed.
Understanding weak collections requires a solid grasp of how JavaScript manages memory. In JavaScript, the garbage collector automatically frees memory occupied by objects that are no longer reachable from the root of the program (the global scope, active function scopes, and so on). With regular Set and Map, storing an object creates a strong reference that keeps the object alive even if no other part of your code references it. Weak collections solve this problem by allowing the garbage collector to do its job regardless of whether the object exists in the collection.
Set and Map. They serve a fundamentally different purpose. Use Set and Map when you need to iterate over elements, check the collection size, or store primitive values. Use WeakSet and WeakMap when you want to attach auxiliary data to objects without interfering with garbage collection.WeakSet -- Creation and Basic Operations
A WeakSet is a collection of objects where each object can appear only once, just like a regular Set. However, a WeakSet only accepts objects as members -- no primitives like strings or numbers are allowed. You create a WeakSet using the new WeakSet() constructor, optionally passing an iterable of objects.
Example: Creating a WeakSet
// Create an empty WeakSet
const ws = new WeakSet();
// Create objects to add
const user = { name: 'Alice' };
const admin = { name: 'Bob', role: 'admin' };
const guest = { name: 'Charlie' };
// Create a WeakSet with initial values
const wsWithValues = new WeakSet([user, admin, guest]);
console.log(wsWithValues.has(user)); // true
console.log(wsWithValues.has(admin)); // true
Adding Elements with add()
The add() method inserts an object into the WeakSet. If the object is already present, the call has no effect. The method returns the WeakSet itself, allowing you to chain multiple add() calls together.
Example: Adding Objects to a WeakSet
const ws = new WeakSet();
const objA = { id: 1 };
const objB = { id: 2 };
const objC = { id: 3 };
// Add objects individually
ws.add(objA);
ws.add(objB);
// Chain add calls
ws.add(objA).add(objC);
console.log(ws.has(objA)); // true
console.log(ws.has(objB)); // true
console.log(ws.has(objC)); // true
// Adding the same object again has no effect
ws.add(objA);
console.log(ws.has(objA)); // still true, no duplicate
Checking Membership with has()
The has() method returns true if the specified object exists in the WeakSet and false otherwise. This is the primary way to query a WeakSet since you cannot iterate over its contents.
Example: Checking if an Object is in a WeakSet
const ws = new WeakSet();
const existing = { status: 'active' };
const missing = { status: 'inactive' };
ws.add(existing);
console.log(ws.has(existing)); // true
console.log(ws.has(missing)); // false
console.log(ws.has({})); // false -- different object reference
ws.has({ status: 'active' }) returns false even if you added an object with the same properties, because it is a different object in memory.Removing Elements with delete()
The delete() method removes an object from the WeakSet. It returns true if the object was found and removed, and false if the object was not in the WeakSet.
Example: Deleting from a WeakSet
const ws = new WeakSet();
const item = { value: 42 };
ws.add(item);
console.log(ws.has(item)); // true
console.log(ws.delete(item)); // true -- successfully removed
console.log(ws.has(item)); // false
console.log(ws.delete(item)); // false -- was not in the set
WeakSet Restrictions
WeakSet has several intentional restrictions that distinguish it from a regular Set. These restrictions exist because of the weak reference semantics and the unpredictable timing of garbage collection.
Only Objects Allowed
You cannot add primitive values (strings, numbers, booleans, symbols, null, undefined, or BigInt) to a WeakSet. Attempting to do so throws a TypeError. This restriction exists because primitives are not garbage collected the same way as objects -- they are managed by value, not by reference.
Example: WeakSet Rejects Primitives
const ws = new WeakSet();
// All of these throw TypeError
try { ws.add(42); } catch (e) { console.log(e.message); }
// "Invalid value used in weak set"
try { ws.add('hello'); } catch (e) { console.log(e.message); }
// "Invalid value used in weak set"
try { ws.add(true); } catch (e) { console.log(e.message); }
// "Invalid value used in weak set"
try { ws.add(null); } catch (e) { console.log(e.message); }
// "Invalid value used in weak set"
// These work fine -- they are objects
ws.add({});
ws.add([]);
ws.add(new Date());
ws.add(new Map());
ws.add(function() {});
No Iteration, No Size
A WeakSet has no size property, no forEach() method, and is not iterable (no for...of, no keys(), values(), or entries()). You cannot enumerate or count the elements in a WeakSet. This is by design: because the garbage collector can remove elements at any time, the contents of a WeakSet are non-deterministic. Exposing iteration would create unpredictable behavior.
Example: WeakSet Is Not Iterable
const ws = new WeakSet();
ws.add({ a: 1 });
ws.add({ b: 2 });
// None of these work
console.log(ws.size); // undefined
// ws.forEach(...) // TypeError: ws.forEach is not a function
// for (const item of ws) // TypeError: ws is not iterable
// [...ws] // TypeError: ws is not iterable
// Array.from(ws) // TypeError: ws is not iterable
// The only operations available are:
// ws.add(obj) -- add an object
// ws.has(obj) -- check if object exists
// ws.delete(obj) -- remove an object
Garbage Collection Behavior
The defining feature of WeakSet is its interaction with the garbage collector. When you add an object to a WeakSet, the WeakSet holds a weak reference to that object. If the object becomes unreachable from anywhere else in your program, the garbage collector is free to reclaim that memory, and the object is automatically removed from the WeakSet. You never need to manually clean up a WeakSet -- it takes care of itself.
Example: Garbage Collection with WeakSet
const ws = new WeakSet();
// Create a function scope to demonstrate garbage collection
function createAndAdd() {
const tempObj = { data: 'temporary' };
ws.add(tempObj);
console.log(ws.has(tempObj)); // true
return; // tempObj goes out of scope here
}
createAndAdd();
// After this call, tempObj is no longer reachable
// The garbage collector MAY remove it from the WeakSet at any time
// Contrast with a strong reference
let keepAlive = { data: 'persistent' };
ws.add(keepAlive);
console.log(ws.has(keepAlive)); // true
// Even after some time, keepAlive remains in ws
// because the variable still references it
keepAlive = null; // Now the object is unreachable
// The garbage collector MAY remove it from the WeakSet
WeakSet Use Cases
Although the restrictions on WeakSet may seem limiting, there are several important patterns where it provides the ideal solution.
Tracking DOM Elements
One of the most practical uses of WeakSet is tracking which DOM elements have been processed by your code. When elements are removed from the DOM, they can be garbage collected, and the WeakSet automatically cleans up without any manual intervention.
Example: Tracking Processed DOM Elements
const processedElements = new WeakSet();
function enhanceElement(element) {
// Skip if already processed
if (processedElements.has(element)) {
return;
}
// Add animation class, attach event listeners, etc.
element.classList.add('enhanced');
element.addEventListener('click', handleClick);
// Mark as processed
processedElements.add(element);
}
// Process all current buttons
document.querySelectorAll('button').forEach(enhanceElement);
// Later, if new buttons are added dynamically
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.tagName === 'BUTTON') {
enhanceElement(node);
}
});
});
});
// If a button is removed from the DOM and no longer referenced,
// it will be automatically garbage collected and removed from
// processedElements -- no memory leak!
Marking Objects as Processed
In data processing pipelines, you may need to track which objects have already been handled to avoid double processing. A WeakSet allows you to flag objects without preventing them from being garbage collected once they flow through the pipeline.
Example: Preventing Double Processing
const validated = new WeakSet();
function validateUser(user) {
if (validated.has(user)) {
console.log('User already validated, skipping.');
return true;
}
// Perform expensive validation
const isValid = user.name && user.email && user.age > 0;
if (isValid) {
validated.add(user);
}
return isValid;
}
const user1 = { name: 'Alice', email: 'alice@example.com', age: 30 };
validateUser(user1); // Performs full validation
validateUser(user1); // Skips -- already validated
// If user1 is later set to null, the garbage collector
// can reclaim its memory without us cleaning up 'validated'
Preventing Circular References in Serialization
When you need to traverse or serialize an object graph that may contain circular references, a WeakSet can track visited objects to prevent infinite loops. Once the traversal is complete, the WeakSet does not prevent the visited objects from being garbage collected.
Example: Detecting Circular References
function deepClone(obj, visited = new WeakSet()) {
// Handle primitives and null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Detect circular reference
if (visited.has(obj)) {
throw new Error('Circular reference detected');
}
// Mark this object as visited
visited.add(obj);
// Clone arrays
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item, visited));
}
// Clone plain objects
const clone = {};
for (const key of Object.keys(obj)) {
clone[key] = deepClone(obj[key], visited);
}
return clone;
}
// Test with circular reference
const parent = { name: 'parent' };
const child = { name: 'child', parent: parent };
parent.child = child; // circular!
try {
deepClone(parent);
} catch (e) {
console.log(e.message); // "Circular reference detected"
}
WeakMap -- Creation and Basic Operations
A WeakMap is a collection of key-value pairs where the keys must be objects and the values can be any type. Like WeakSet, a WeakMap holds weak references to its keys, meaning entries are automatically removed when the key object is garbage collected. This makes WeakMap the perfect tool for associating private or auxiliary data with objects.
Example: Creating a WeakMap
// Create an empty WeakMap
const wm = new WeakMap();
// Create key objects
const key1 = { id: 1 };
const key2 = { id: 2 };
const key3 = { id: 3 };
// Create a WeakMap with initial entries
const wmWithEntries = new WeakMap([
[key1, 'value one'],
[key2, 'value two'],
[key3, 100]
]);
console.log(wmWithEntries.get(key1)); // "value one"
console.log(wmWithEntries.get(key2)); // "value two"
console.log(wmWithEntries.get(key3)); // 100
Setting Values with set()
The set() method adds or updates an entry in the WeakMap. The key must be an object; the value can be any JavaScript value. The method returns the WeakMap itself, enabling method chaining.
Example: Setting Entries in a WeakMap
const wm = new WeakMap();
const user = { name: 'Alice' };
const product = { sku: 'ABC-123' };
// Set individual entries
wm.set(user, { role: 'admin', loginCount: 42 });
wm.set(product, { price: 29.99, inStock: true });
// Chain set calls
const session = {};
const config = {};
wm.set(session, 'abc123').set(config, { theme: 'dark' });
// Update an existing entry
wm.set(user, { role: 'superadmin', loginCount: 43 });
console.log(wm.get(user)); // { role: "superadmin", loginCount: 43 }
Getting Values with get()
The get() method retrieves the value associated with a given key object. If the key does not exist in the WeakMap, it returns undefined.
Example: Retrieving Values from a WeakMap
const wm = new WeakMap();
const element = document.createElement('div');
wm.set(element, { clicks: 0, lastClicked: null });
// Get the value
const data = wm.get(element);
console.log(data); // { clicks: 0, lastClicked: null }
// Modify the retrieved value
data.clicks++;
data.lastClicked = new Date();
// The change persists because objects are references
console.log(wm.get(element).clicks); // 1
// Non-existent key returns undefined
console.log(wm.get({})); // undefined
Checking Keys with has()
The has() method returns true if the WeakMap contains an entry for the specified key and false otherwise.
Example: Checking for Keys in a WeakMap
const wm = new WeakMap();
const keyObj = { type: 'config' };
wm.set(keyObj, { debug: true });
console.log(wm.has(keyObj)); // true
console.log(wm.has({})); // false -- different reference
Deleting Entries with delete()
The delete() method removes the entry for a given key. It returns true if an entry was found and deleted, and false otherwise.
Example: Deleting WeakMap Entries
const wm = new WeakMap();
const obj = { x: 10 };
wm.set(obj, 'some data');
console.log(wm.has(obj)); // true
console.log(wm.delete(obj)); // true
console.log(wm.has(obj)); // false
console.log(wm.delete(obj)); // false -- already removed
WeakMap Restrictions
WeakMap shares similar restrictions with WeakSet, and they exist for the same reasons -- the non-deterministic nature of garbage collection makes enumeration unreliable.
- Keys must be objects. Primitives like strings, numbers, and booleans cannot be used as WeakMap keys. Attempting to use a primitive key throws a
TypeError. - No size property. You cannot determine how many entries are in a WeakMap.
- No iteration. WeakMap has no
forEach(),keys(),values(),entries(), or[Symbol.iterator]. You cannot loop over its contents. - No clear() method. Unlike
Map, you cannot clear all entries from aWeakMapat once. To effectively "clear" a WeakMap, you must create a new one.
Example: WeakMap Key Restrictions
const wm = new WeakMap();
// These throw TypeError -- primitives are not valid keys
try { wm.set('key', 'value'); } catch (e) { console.log(e.message); }
try { wm.set(42, 'value'); } catch (e) { console.log(e.message); }
try { wm.set(true, 'value'); } catch (e) { console.log(e.message); }
try { wm.set(Symbol(), 'value'); } catch (e) { console.log(e.message); }
// These work -- objects as keys, any type as values
wm.set({}, 'string value');
wm.set([], 42);
wm.set(function() {}, true);
wm.set(new Date(), null);
wm.set(document.body, [1, 2, 3]);
Private Data with WeakMap
One of the most powerful use cases for WeakMap is implementing truly private data for objects. Before private class fields (the # syntax) were available, WeakMap was the standard pattern for achieving encapsulation in JavaScript. Even today, WeakMap-based privacy remains useful in many scenarios, especially when working outside of class syntax.
Example: Private Data Using WeakMap
// Module scope -- the WeakMap is not accessible from outside
const _privateData = new WeakMap();
class User {
constructor(name, email, password) {
// Public properties
this.name = name;
this.email = email;
// Private data stored in the WeakMap
_privateData.set(this, {
password: password,
loginAttempts: 0,
lastLogin: null
});
}
authenticate(password) {
const priv = _privateData.get(this);
priv.loginAttempts++;
if (priv.password === password) {
priv.lastLogin = new Date();
return true;
}
return false;
}
getLoginInfo() {
const priv = _privateData.get(this);
return {
attempts: priv.loginAttempts,
lastLogin: priv.lastLogin
// password is NOT exposed
};
}
}
const user = new User('Alice', 'alice@example.com', 'secret123');
console.log(user.name); // "Alice" -- public
console.log(user.password); // undefined -- not on the object!
user.authenticate('wrong');
user.authenticate('secret123');
console.log(user.getLoginInfo());
// { attempts: 2, lastLogin: [Date] }
// When user is garbage collected, the private data
// is automatically cleaned up from _privateData
Caching with WeakMap
WeakMap is an excellent tool for implementing caches that do not cause memory leaks. You can associate computed results with objects, and when the objects are no longer needed, the cached values are automatically cleaned up. This is known as memoization with automatic cache eviction.
Example: WeakMap-Based Cache
const cache = new WeakMap();
function expensiveComputation(obj) {
// Check cache first
if (cache.has(obj)) {
console.log('Returning cached result');
return cache.get(obj);
}
// Simulate expensive work
console.log('Computing result...');
const result = {
hash: JSON.stringify(obj).split('').reduce(
(acc, char) => acc + char.charCodeAt(0), 0
),
processed: true,
timestamp: Date.now()
};
// Store in cache
cache.set(obj, result);
return result;
}
const data1 = { values: [1, 2, 3, 4, 5] };
const data2 = { values: [10, 20, 30] };
expensiveComputation(data1); // "Computing result..."
expensiveComputation(data1); // "Returning cached result"
expensiveComputation(data2); // "Computing result..."
// When data1 and data2 are garbage collected,
// their cached results are automatically removed
// No manual cache invalidation needed!
Example: Caching DOM Element Measurements
const measurementCache = new WeakMap();
function getElementDimensions(element) {
if (measurementCache.has(element)) {
return measurementCache.get(element);
}
// getBoundingClientRect is relatively expensive
const rect = element.getBoundingClientRect();
const dimensions = {
width: rect.width,
height: rect.height,
top: rect.top,
left: rect.left
};
measurementCache.set(element, dimensions);
return dimensions;
}
// When elements are removed from the DOM and dereferenced,
// the cached measurements are automatically cleaned up
WeakRef and FinalizationRegistry (Brief Introduction)
ES2021 introduced two complementary features: WeakRef and FinalizationRegistry. These provide lower-level control over weak references and allow you to run cleanup callbacks when objects are garbage collected. While they are related to WeakSet and WeakMap, they serve more advanced use cases and should be used sparingly.
Example: WeakRef Basics
// WeakRef creates a weak reference to an object
let targetObj = { data: 'important' };
const weakRef = new WeakRef(targetObj);
// deref() returns the object if it is still alive
console.log(weakRef.deref()); // { data: "important" }
// After the strong reference is removed...
targetObj = null;
// At some point after garbage collection:
// weakRef.deref() will return undefined
// But you cannot predict exactly when!
Example: FinalizationRegistry Basics
// FinalizationRegistry runs a callback when registered objects
// are garbage collected
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object with identifier "${heldValue}" was collected`);
// Perform cleanup: close connections, release resources, etc.
});
let connection = { url: 'wss://example.com', socket: {} };
registry.register(connection, 'websocket-connection-1');
// When connection is garbage collected, the callback fires
connection = null;
// Eventually logs: Object with identifier "websocket-connection-1" was collected
WeakRef and FinalizationRegistry should be avoided in most application code. The garbage collector's behavior is non-deterministic, so you cannot rely on the timing of deref() returning undefined or finalization callbacks firing. Use them only for advanced scenarios like custom caches, resource management, or debugging. The TC39 proposal documentation explicitly warns: "Correct use of FinalizationRegistry requires careful thought, and best avoided if possible."Comparison: Set vs WeakSet, Map vs WeakMap
Understanding the differences between the strong and weak variants of these collections is essential for choosing the right tool. The following comparison covers all the key distinctions.
Reference: Set vs WeakSet
// ┌──────────────────────┬────────────────────┬────────────────────┐
// │ Feature │ Set │ WeakSet │
// ├──────────────────────┼────────────────────┼────────────────────┤
// │ Key/Value types │ Any value │ Objects only │
// │ Reference type │ Strong │ Weak │
// │ Garbage collection │ Prevents GC │ Allows GC │
// │ size property │ Yes │ No │
// │ Iterable │ Yes │ No │
// │ forEach() │ Yes │ No │
// │ keys()/values() │ Yes │ No │
// │ entries() │ Yes │ No │
// │ clear() │ Yes │ No │
// │ add() │ Yes │ Yes │
// │ has() │ Yes │ Yes │
// │ delete() │ Yes │ Yes │
// │ Use case │ Unique collection │ Tagging objects │
// └──────────────────────┴────────────────────┴────────────────────┘
Reference: Map vs WeakMap
// ┌──────────────────────┬────────────────────┬────────────────────┐
// │ Feature │ Map │ WeakMap │
// ├──────────────────────┼────────────────────┼────────────────────┤
// │ Key types │ Any value │ Objects only │
// │ Value types │ Any value │ Any value │
// │ Reference type │ Strong │ Weak (keys only) │
// │ Garbage collection │ Prevents GC │ Allows GC of keys │
// │ size property │ Yes │ No │
// │ Iterable │ Yes │ No │
// │ forEach() │ Yes │ No │
// │ keys()/values() │ Yes │ No │
// │ entries() │ Yes │ No │
// │ clear() │ Yes │ No │
// │ set()/get() │ Yes │ Yes │
// │ has() │ Yes │ Yes │
// │ delete() │ Yes │ Yes │
// │ Use case │ Key-value store │ Private/meta data │
// └──────────────────────┴────────────────────┴────────────────────┘
Example: Choosing the Right Collection
// Use Set when you need a unique collection of any values
const uniqueTags = new Set(['javascript', 'css', 'html']);
console.log(uniqueTags.size); // 3
// Use WeakSet when you need to tag objects temporarily
const visited = new WeakSet();
function visitNode(node) {
if (visited.has(node)) return;
visited.add(node);
// process node...
}
// Use Map when you need key-value pairs with any key type
const settings = new Map();
settings.set('theme', 'dark');
settings.set(42, 'the answer');
// Use WeakMap when you associate data with objects
// and want automatic cleanup
const metadata = new WeakMap();
function attachMeta(obj, meta) {
metadata.set(obj, meta);
}
// When obj is GC'd, meta is cleaned up automatically
Real-World Patterns
Let us look at some advanced real-world patterns that combine WeakSet and WeakMap to solve practical problems you will encounter in production code.
Example: Event Listener Tracker (WeakMap + WeakSet)
// Track which listeners are attached to which elements
const listenerMap = new WeakMap();
function addTrackedListener(element, event, handler) {
// Get or create the set of events for this element
if (!listenerMap.has(element)) {
listenerMap.set(element, new Map());
}
const events = listenerMap.get(element);
// Get or create the set of handlers for this event
if (!events.has(event)) {
events.set(event, new WeakSet());
}
const handlers = events.get(event);
// Only add if not already attached
if (!handlers.has(handler)) {
element.addEventListener(event, handler);
handlers.add(handler);
}
}
function removeTrackedListener(element, event, handler) {
const events = listenerMap.get(element);
if (!events) return;
const handlers = events.get(event);
if (!handlers) return;
if (handlers.has(handler)) {
element.removeEventListener(event, handler);
handlers.delete(handler);
}
}
// Usage
const button = document.createElement('button');
const clickHandler = () => console.log('clicked');
addTrackedListener(button, 'click', clickHandler);
addTrackedListener(button, 'click', clickHandler); // ignored, already added
// When button is removed from DOM and dereferenced, everything is cleaned up
Example: Object Access Counter with WeakMap
const accessCounter = new WeakMap();
function trackAccess(obj) {
const count = accessCounter.get(obj) || 0;
accessCounter.set(obj, count + 1);
return obj;
}
function getAccessCount(obj) {
return accessCounter.get(obj) || 0;
}
const resource = { type: 'image', src: '/photo.jpg' };
trackAccess(resource);
trackAccess(resource);
trackAccess(resource);
console.log(getAccessCount(resource)); // 3
// When resource is no longer referenced,
// the counter is automatically garbage collected
Practice Exercise
Build a small application that demonstrates the use of both WeakSet and WeakMap. Create a TaskManager class that manages task objects. Use a WeakSet called completedTasks to track which tasks have been completed, and a WeakMap called taskMetadata to store private metadata (creation timestamp, priority level, assigned user) for each task. Implement the following methods: (1) addTask(task) that registers a new task with metadata. (2) completeTask(task) that marks a task as complete using the WeakSet. (3) isCompleted(task) that checks whether a task is completed. (4) getMetadata(task) that returns the private metadata for a task without exposing internal state. (5) removeTask(task) that removes a task from both the WeakSet and WeakMap. Then write a function called deepEqual(obj1, obj2, visited) that uses a WeakSet to track visited objects and correctly handles circular references during deep comparison. Test your code by creating tasks, completing some, checking their status, retrieving metadata, and verifying that circular reference detection works properly.