WeakMap and WeakSet hold weak references to objects, enabling garbage collection. Here's how to use them.
WeakMap vs Map#
1// Map: Strong references, prevents garbage collection
2const map = new Map();
3let obj = { name: 'John' };
4map.set(obj, 'metadata');
5obj = null; // Object still exists in map
6
7// WeakMap: Weak references, allows garbage collection
8const weakMap = new WeakMap();
9let obj2 = { name: 'Jane' };
10weakMap.set(obj2, 'metadata');
11obj2 = null; // Object can be garbage collected
12
13// Key differences
14const map = new Map();
15map.set('string', 'value'); // OK
16map.set(123, 'value'); // OK
17map.size; // Available
18
19const weakMap = new WeakMap();
20// weakMap.set('string', 'value'); // Error: Invalid key
21weakMap.set({}, 'value'); // Only objects as keys
22// weakMap.size; // Not available
23// weakMap.keys(); // Not iterableWeakSet vs Set#
1// Set: Strong references
2const set = new Set();
3let obj = { id: 1 };
4set.add(obj);
5obj = null; // Object still exists in set
6
7// WeakSet: Weak references
8const weakSet = new WeakSet();
9let obj2 = { id: 2 };
10weakSet.add(obj2);
11obj2 = null; // Object can be garbage collected
12
13// WeakSet limitations
14const weakSet = new WeakSet();
15// weakSet.add('string'); // Error: Invalid value
16// weakSet.size; // Not available
17// weakSet.values(); // Not iterable
18weakSet.add({}); // OK
19weakSet.has({}); // false (different object)Private Data Pattern#
1// Store private data without exposing it
2const privateData = new WeakMap();
3
4class User {
5 constructor(name, password) {
6 this.name = name;
7 privateData.set(this, { password });
8 }
9
10 checkPassword(attempt) {
11 const data = privateData.get(this);
12 return data.password === attempt;
13 }
14
15 // Password is not accessible from outside
16 // When User instance is garbage collected,
17 // private data is also cleaned up
18}
19
20const user = new User('John', 'secret123');
21console.log(user.name); // 'John'
22console.log(user.password); // undefined
23console.log(user.checkPassword('secret123')); // true
24
25// With Symbol (alternative but data visible via reflection)
26const _password = Symbol('password');
27class User2 {
28 constructor(name, password) {
29 this.name = name;
30 this[_password] = password;
31 }
32}
33// Object.getOwnPropertySymbols(user2) exposes the symbolCaching with WeakMap#
1// Cache computation results per object
2const cache = new WeakMap();
3
4function expensiveComputation(obj) {
5 if (cache.has(obj)) {
6 return cache.get(obj);
7 }
8
9 // Expensive operation
10 const result = Object.keys(obj).reduce((sum, key) => {
11 return sum + String(obj[key]).length;
12 }, 0);
13
14 cache.set(obj, result);
15 return result;
16}
17
18const data = { a: 'hello', b: 'world' };
19expensiveComputation(data); // Computes
20expensiveComputation(data); // Returns cached
21
22// When data is no longer referenced,
23// cache entry is automatically cleaned up
24
25// Memoization for class instances
26const memoCache = new WeakMap();
27
28function memoize(instance, key, fn) {
29 if (!memoCache.has(instance)) {
30 memoCache.set(instance, new Map());
31 }
32
33 const instanceCache = memoCache.get(instance);
34
35 if (!instanceCache.has(key)) {
36 instanceCache.set(key, fn());
37 }
38
39 return instanceCache.get(key);
40}DOM Element Metadata#
1// Associate data with DOM elements
2const elementData = new WeakMap();
3
4function setElementData(element, data) {
5 elementData.set(element, data);
6}
7
8function getElementData(element) {
9 return elementData.get(element);
10}
11
12// Usage
13const button = document.querySelector('#myButton');
14setElementData(button, {
15 clickCount: 0,
16 lastClicked: null,
17});
18
19button.addEventListener('click', () => {
20 const data = getElementData(button);
21 data.clickCount++;
22 data.lastClicked = new Date();
23});
24
25// When button is removed from DOM and dereferenced,
26// metadata is automatically cleaned up
27
28// Event listener tracking
29const listeners = new WeakMap();
30
31function addTrackedListener(element, event, handler) {
32 if (!listeners.has(element)) {
33 listeners.set(element, []);
34 }
35
36 element.addEventListener(event, handler);
37 listeners.get(element).push({ event, handler });
38}
39
40function removeAllListeners(element) {
41 const elementListeners = listeners.get(element) || [];
42 elementListeners.forEach(({ event, handler }) => {
43 element.removeEventListener(event, handler);
44 });
45 listeners.delete(element);
46}Object Visited Tracking#
1// Track visited objects in recursive operations
2function deepClone(obj, visited = new WeakSet()) {
3 // Handle primitives
4 if (obj === null || typeof obj !== 'object') {
5 return obj;
6 }
7
8 // Handle circular references
9 if (visited.has(obj)) {
10 return obj; // Or throw error, or return placeholder
11 }
12
13 visited.add(obj);
14
15 // Handle arrays
16 if (Array.isArray(obj)) {
17 return obj.map(item => deepClone(item, visited));
18 }
19
20 // Handle objects
21 const clone = {};
22 for (const key in obj) {
23 if (obj.hasOwnProperty(key)) {
24 clone[key] = deepClone(obj[key], visited);
25 }
26 }
27
28 return clone;
29}
30
31// Test with circular reference
32const obj = { a: 1, b: { c: 2 } };
33obj.self = obj;
34const cloned = deepClone(obj);
35
36// Compare objects without infinite loop
37function deepEqual(a, b, visited = new WeakSet()) {
38 if (a === b) return true;
39 if (typeof a !== 'object' || typeof b !== 'object') return false;
40 if (a === null || b === null) return false;
41
42 if (visited.has(a)) return true; // Assume equal for circular
43 visited.add(a);
44
45 const keysA = Object.keys(a);
46 const keysB = Object.keys(b);
47
48 if (keysA.length !== keysB.length) return false;
49
50 return keysA.every(key =>
51 keysB.includes(key) && deepEqual(a[key], b[key], visited)
52 );
53}WeakRef for Optional Caching#
1// WeakRef provides weak reference to single object
2class Cache {
3 constructor() {
4 this.cache = new Map();
5 }
6
7 set(key, value) {
8 this.cache.set(key, new WeakRef(value));
9 }
10
11 get(key) {
12 const ref = this.cache.get(key);
13 if (ref) {
14 const value = ref.deref();
15 if (value !== undefined) {
16 return value;
17 }
18 // Clean up dead reference
19 this.cache.delete(key);
20 }
21 return undefined;
22 }
23
24 has(key) {
25 return this.get(key) !== undefined;
26 }
27}
28
29// FinalizationRegistry for cleanup callbacks
30const registry = new FinalizationRegistry((heldValue) => {
31 console.log(`Object with id ${heldValue} was garbage collected`);
32});
33
34function createTrackedObject(id) {
35 const obj = { id };
36 registry.register(obj, id);
37 return obj;
38}
39
40let tracked = createTrackedObject(1);
41tracked = null; // Eventually logs: "Object with id 1 was garbage collected"Instance Counting#
1// Track active instances without preventing GC
2const instances = new WeakSet();
3let instanceCount = 0;
4
5const registry = new FinalizationRegistry(() => {
6 instanceCount--;
7 console.log(`Instance collected. Active: ${instanceCount}`);
8});
9
10class TrackedClass {
11 constructor() {
12 instances.add(this);
13 instanceCount++;
14 registry.register(this, undefined);
15 console.log(`Instance created. Active: ${instanceCount}`);
16 }
17
18 static isInstance(obj) {
19 return instances.has(obj);
20 }
21}
22
23let a = new TrackedClass(); // Active: 1
24let b = new TrackedClass(); // Active: 2
25TrackedClass.isInstance(a); // true
26
27a = null; // Eventually: Active: 1
28b = null; // Eventually: Active: 0React Integration#
1// Store component-specific data
2const componentData = new WeakMap();
3
4function useComponentData<T>(initialData: T) {
5 const ref = useRef<object>({});
6
7 if (!componentData.has(ref.current)) {
8 componentData.set(ref.current, initialData);
9 }
10
11 return componentData.get(ref.current) as T;
12}
13
14// Track mounted components
15const mountedComponents = new WeakSet();
16
17function useTrackMounted() {
18 const ref = useRef<object>({});
19
20 useEffect(() => {
21 mountedComponents.add(ref.current);
22 return () => {
23 mountedComponents.delete(ref.current);
24 };
25 }, []);
26
27 return ref;
28}API Response Caching#
1// Cache API responses per request config
2const responseCache = new WeakMap();
3
4async function cachedFetch(config) {
5 // config must be an object (not a string URL)
6 if (responseCache.has(config)) {
7 return responseCache.get(config);
8 }
9
10 const response = await fetch(config.url, config.options);
11 const data = await response.json();
12
13 responseCache.set(config, data);
14 return data;
15}
16
17// Config objects as keys
18const userConfig = { url: '/api/users', options: { method: 'GET' } };
19await cachedFetch(userConfig); // Fetches
20await cachedFetch(userConfig); // Returns cached
21
22// When userConfig is no longer referenced,
23// cached response is cleaned upBest Practices#
Use WeakMap when:
✓ Storing metadata for objects
✓ Implementing private data
✓ Caching computed values
✓ Associating data with DOM elements
Use WeakSet when:
✓ Tracking visited objects
✓ Marking objects as processed
✓ Checking object membership
✓ Preventing duplicate processing
Avoid when:
✗ Need to iterate over entries
✗ Need to know size
✗ Keys are primitives
✗ Need persistent storage
Conclusion#
WeakMap and WeakSet enable memory-efficient object associations. Use WeakMap for private data, caching, and metadata. Use WeakSet for tracking processed objects. Both automatically clean up when objects are garbage collected, preventing memory leaks.