Back to Blog
JavaScriptWeakMapWeakSetMemory

JavaScript WeakMap and WeakSet Guide

Master WeakMap and WeakSet in JavaScript. From memory management to caching to private data patterns.

B
Bootspring Team
Engineering
April 30, 2021
7 min read

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 iterable

WeakSet 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 symbol

Caching 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: 0

React 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 up

Best 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.

Share this article

Help spread the word about Bootspring