Difficulty: Advanced
WeakMap and WeakSet are specialized collection types that hold 'weak' references to their keys (WeakMap) or values (WeakSet). A weak reference does not prevent the garbage collector from reclaiming the referenced object when no other strong references to it exist. This fundamental property makes WeakMap and WeakSet essential tools for memory-conscious programming patterns where you need to associate data with objects without creating memory leaks.
WeakMap is a collection of key-value pairs where keys must be objects (or non-registered symbols). Unlike Map, WeakMap does not prevent its keys from being garbage collected. If the only remaining reference to an object is as a WeakMap key, the garbage collector can reclaim both the object and its associated value. This makes WeakMap ideal for storing metadata about objects that should be cleaned up automatically when the object is no longer needed.
The most common use case for WeakMap is storing private data associated with objects. Since class fields marked with # are relatively new, WeakMap has traditionally been used to implement true privacy in JavaScript. You create a module-scoped WeakMap and use instances as keys to store their private data. External code cannot access the WeakMap, and when an instance is garbage collected, its private data is automatically cleaned up.
WeakMap is also excellent for caching computed results. If you cache the result of an expensive computation using a regular Map, the cached entries persist forever, even after the input objects are no longer used elsewhere. With WeakMap, cached entries are automatically released when the input objects are garbage collected, preventing the cache from growing unboundedly. Similarly, WeakMap is perfect for attaching metadata to DOM elements - when an element is removed from the DOM and garbage collected, its associated metadata is automatically freed.
WeakSet holds a collection of objects (only objects, no primitives) with weak references. Like WeakMap, if the only reference to an object is in a WeakSet, it can be garbage collected. WeakSet is useful for 'tagging' or 'marking' objects - tracking whether you've seen an object before, preventing circular reference issues in recursive algorithms, or checking if an object belongs to a certain category.
Both WeakMap and WeakSet have significant restrictions compared to their strong counterparts. They are not iterable - you cannot use for...of, spread, or get a list of keys/values. They have no size property and no clear() method. These limitations exist because the contents can change unpredictably as the garbage collector runs, making enumeration unreliable. You can only use get, set, has, and delete operations, which is sufficient for their intended use cases.
// Private data pattern using WeakMap
const _privateData = new WeakMap();
class User {
constructor(name, email) {
// Store private data keyed by this instance
_privateData.set(this, {
email: email,
loginCount: 0,
lastLogin: null
});
this.name = name; // public property
}
login() {
const data = _privateData.get(this);
data.loginCount++;
data.lastLogin = new Date().toISOString();
console.log(`${this.name} logged in (count: ${data.loginCount})`);
}
getEmail() {
return _privateData.get(this).email;
}
getStats() {
const data = _privateData.get(this);
return `Logins: ${data.loginCount}, Last: ${data.lastLogin}`;
}
}
const alice = new User('Alice', 'alice@example.com');
alice.login();
alice.login();
console.log('Email:', alice.getEmail());
console.log('Stats:', alice.getStats());
// Private data is not accessible from outside
console.log('\nPublic properties:', Object.keys(alice));
console.log('Cannot access _privateData directly from alice');
The WeakMap stores private data keyed by each instance. External code can see alice.name but cannot access the email, loginCount, or lastLogin. When the User instance is garbage collected, the WeakMap entry is automatically freed.
// Caching expensive computations
const cache = new WeakMap();
function expensiveProcess(obj) {
if (cache.has(obj)) {
console.log('Cache hit!');
return cache.get(obj);
}
console.log('Computing...');
const result = {
processed: true,
hash: JSON.stringify(obj).length * 17,
timestamp: Date.now()
};
cache.set(obj, result);
return result;
}
const data = { items: [1, 2, 3], total: 6 };
console.log(expensiveProcess(data)); // computes
console.log(expensiveProcess(data)); // cache hit
// When data is dereferenced, cache entry is freed automatically
// data = null; // cache entry becomes eligible for GC
// DOM metadata pattern (conceptual)
console.log('\n--- DOM metadata pattern ---');
const elementData = new WeakMap();
// Simulating DOM elements as objects
const button = { tagName: 'BUTTON', id: 'submit-btn' };
const input = { tagName: 'INPUT', id: 'email-input' };
elementData.set(button, {
clickCount: 0,
handlers: ['onClick', 'onHover'],
created: Date.now()
});
elementData.set(input, {
validations: ['required', 'email'],
dirty: false
});
console.log('Button data:', elementData.get(button));
console.log('Input data:', elementData.get(input));
console.log('Has button:', elementData.has(button));
The cache automatically cleans up when input objects are garbage collected, preventing memory leaks. The DOM metadata pattern attaches data to elements without modifying them - when elements are removed from the DOM and GC'd, the metadata is freed too.
// Track visited objects to prevent infinite recursion
function deepClone(obj, visited = new WeakSet()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Detect circular references
if (visited.has(obj)) {
console.log('Circular reference detected, returning null');
return null;
}
visited.add(obj);
const clone = Array.isArray(obj) ? [] : {};
for (const key of Object.keys(obj)) {
clone[key] = deepClone(obj[key], visited);
}
return clone;
}
// Test with circular reference
const original = { name: 'Alice', nested: { value: 42 } };
original.self = original; // circular!
const cloned = deepClone(original);
console.log('Cloned name:', cloned.name);
console.log('Cloned nested:', cloned.nested);
console.log('Cloned self:', cloned.self);
// Mark objects as "processed"
const processed = new WeakSet();
function processOnce(item) {
if (processed.has(item)) {
console.log(`Already processed: ${item.id}`);
return;
}
processed.add(item);
console.log(`Processing: ${item.id}`);
}
const task1 = { id: 'task-1' };
const task2 = { id: 'task-2' };
processOnce(task1);
processOnce(task2);
processOnce(task1); // already done
processOnce(task1); // already done
WeakSet tracks visited objects during deep clone, detecting circular references without creating memory leaks. The processOnce pattern ensures each object is processed only once - when the object is GC'd, the WeakSet entry is automatically removed.
// WeakMap restrictions
const wm = new WeakMap();
// Keys must be objects
try {
wm.set('string-key', 'value');
} catch (e) {
console.log('WeakMap key error:', e.message);
}
// Valid keys
const obj = {};
const arr = [];
const func = () => {};
wm.set(obj, 'object value');
wm.set(arr, 'array value');
wm.set(func, 'function value');
console.log('Object value:', wm.get(obj));
console.log('Array value:', wm.get(arr));
// WeakSet restrictions
const ws = new WeakSet();
try {
ws.add(42);
} catch (e) {
console.log('\nWeakSet value error:', e.message);
}
ws.add(obj);
console.log('Has obj:', ws.has(obj));
// What WeakMap/WeakSet DON'T have
console.log('\n--- Missing features ---');
console.log('No .size property');
console.log('No .keys() / .values() / .entries()');
console.log('No .forEach()');
console.log('No iteration (for...of)');
console.log('No .clear()');
console.log('\nAvailable: get, set, has, delete (WeakMap)');
console.log('Available: add, has, delete (WeakSet)');
// Comparison summary
const features = [
['Feature', 'Map/Set', 'WeakMap/WeakSet'],
['Key types', 'any', 'objects only'],
['Iterable', 'yes', 'no'],
['size', 'yes', 'no'],
['GC cleanup', 'no', 'yes'],
['Use case', 'general', 'metadata/caching']
];
console.log('\n--- Comparison ---');
features.forEach(row => {
console.log(`${row[0].padEnd(15)} ${row[1].padEnd(15)} ${row[2]}`);
});
WeakMap and WeakSet only accept objects as keys/values and are not iterable. These restrictions exist because garbage collection is non-deterministic - the contents could change at any time, making enumeration unreliable. The tradeoff is automatic memory management.
WeakMap, WeakSet, weak references, garbage collection, private data, caching, DOM metadata