Back to Blog
JavaScriptWeakMapWeakSetMemory

JavaScript WeakMap and WeakSet Guide

Master JavaScript WeakMap and WeakSet for memory-efficient object associations and tracking.

B
Bootspring Team
Engineering
January 3, 2019
6 min read

WeakMap and WeakSet hold weak references to objects, allowing garbage collection when no other references exist. Here's how to use them.

WeakMap Basics#

1// Create WeakMap 2const weakMap = new WeakMap(); 3 4// Only objects as keys (no primitives) 5const obj = { id: 1 }; 6const func = () => {}; 7const arr = [1, 2, 3]; 8 9weakMap.set(obj, 'object data'); 10weakMap.set(func, 'function data'); 11weakMap.set(arr, 'array data'); 12 13console.log(weakMap.get(obj)); // 'object data' 14console.log(weakMap.has(func)); // true 15 16weakMap.delete(arr); 17console.log(weakMap.has(arr)); // false 18 19// Primitives not allowed 20// weakMap.set('string', 'data'); // TypeError

Private Data Pattern#

1// Store private data for class instances 2const privateData = new WeakMap(); 3 4class User { 5 constructor(name, password) { 6 // Public data 7 this.name = name; 8 9 // Private data - not accessible from outside 10 privateData.set(this, { 11 password, 12 loginAttempts: 0, 13 }); 14 } 15 16 checkPassword(attempt) { 17 const data = privateData.get(this); 18 data.loginAttempts++; 19 20 if (data.loginAttempts > 3) { 21 throw new Error('Too many attempts'); 22 } 23 24 return data.password === attempt; 25 } 26 27 getLoginAttempts() { 28 return privateData.get(this).loginAttempts; 29 } 30} 31 32const user = new User('john', 'secret123'); 33console.log(user.name); // 'john' 34console.log(user.password); // undefined 35console.log(user.checkPassword('wrong')); // false

Caching with WeakMap#

1// Cache computed values without memory leaks 2const cache = new WeakMap(); 3 4function expensiveOperation(obj) { 5 if (cache.has(obj)) { 6 console.log('Cache hit'); 7 return cache.get(obj); 8 } 9 10 console.log('Computing...'); 11 const result = { 12 computed: Object.keys(obj).length, 13 timestamp: Date.now(), 14 }; 15 16 cache.set(obj, result); 17 return result; 18} 19 20const data = { a: 1, b: 2, c: 3 }; 21 22expensiveOperation(data); // Computing... 23expensiveOperation(data); // Cache hit 24 25// When data is no longer referenced, 26// cache entry is automatically garbage collected

DOM Element Data#

1// Associate data with DOM elements 2const elementData = new WeakMap(); 3 4function setElementData(element, data) { 5 const existing = elementData.get(element) || {}; 6 elementData.set(element, { ...existing, ...data }); 7} 8 9function getElementData(element) { 10 return elementData.get(element) || {}; 11} 12 13// Usage 14const button = document.querySelector('#myButton'); 15setElementData(button, { clicks: 0, lastClicked: null }); 16 17button.addEventListener('click', () => { 18 const data = getElementData(button); 19 setElementData(button, { 20 clicks: data.clicks + 1, 21 lastClicked: Date.now(), 22 }); 23}); 24 25// When button is removed from DOM and no longer referenced, 26// associated data is automatically cleaned up

WeakSet Basics#

1// Create WeakSet 2const weakSet = new WeakSet(); 3 4const obj1 = { id: 1 }; 5const obj2 = { id: 2 }; 6 7weakSet.add(obj1); 8weakSet.add(obj2); 9 10console.log(weakSet.has(obj1)); // true 11console.log(weakSet.has({ id: 1 })); // false (different object) 12 13weakSet.delete(obj1); 14console.log(weakSet.has(obj1)); // false 15 16// No iteration, no size property 17// weakSet.forEach() // Error 18// weakSet.size // undefined

Object Tracking#

1// Track which objects have been processed 2const processed = new WeakSet(); 3 4function processOnce(obj) { 5 if (processed.has(obj)) { 6 console.log('Already processed'); 7 return; 8 } 9 10 // Process the object 11 console.log('Processing:', obj); 12 // ... do work ... 13 14 processed.add(obj); 15} 16 17const item = { data: 'test' }; 18processOnce(item); // Processing: { data: 'test' } 19processOnce(item); // Already processed

Circular Reference Detection#

1// Detect circular references during traversal 2function deepClone(obj, seen = new WeakSet()) { 3 if (obj === null || typeof obj !== 'object') { 4 return obj; 5 } 6 7 if (seen.has(obj)) { 8 throw new Error('Circular reference detected'); 9 } 10 11 seen.add(obj); 12 13 if (Array.isArray(obj)) { 14 return obj.map((item) => deepClone(item, seen)); 15 } 16 17 const clone = {}; 18 for (const key in obj) { 19 if (obj.hasOwnProperty(key)) { 20 clone[key] = deepClone(obj[key], seen); 21 } 22 } 23 24 return clone; 25} 26 27// Test 28const a = { name: 'a' }; 29const b = { name: 'b', ref: a }; 30a.ref = b; // Circular reference 31 32try { 33 deepClone(a); // Error: Circular reference detected 34} catch (e) { 35 console.log(e.message); 36}

Instance Validation#

1// Track valid instances 2const validInstances = new WeakSet(); 3 4class SecureToken { 5 constructor(value) { 6 this.value = value; 7 validInstances.add(this); 8 } 9 10 static isValid(token) { 11 return validInstances.has(token); 12 } 13 14 revoke() { 15 validInstances.delete(this); 16 } 17} 18 19function useToken(token) { 20 if (!SecureToken.isValid(token)) { 21 throw new Error('Invalid or revoked token'); 22 } 23 return token.value; 24} 25 26const token = new SecureToken('secret'); 27console.log(useToken(token)); // 'secret' 28 29token.revoke(); 30useToken(token); // Error: Invalid or revoked token

Event Listener Tracking#

1// Track elements with listeners to prevent duplicates 2const hasListener = new WeakSet(); 3 4function addClickHandler(element, handler) { 5 if (hasListener.has(element)) { 6 console.log('Listener already added'); 7 return; 8 } 9 10 element.addEventListener('click', handler); 11 hasListener.add(element); 12} 13 14function removeClickHandler(element, handler) { 15 if (!hasListener.has(element)) { 16 return; 17 } 18 19 element.removeEventListener('click', handler); 20 hasListener.delete(element); 21}

Memoization with Object Keys#

1// Memoize function calls with object arguments 2function memoize(fn) { 3 const cache = new WeakMap(); 4 5 return function (obj) { 6 if (cache.has(obj)) { 7 return cache.get(obj); 8 } 9 10 const result = fn(obj); 11 cache.set(obj, result); 12 return result; 13 }; 14} 15 16// Usage 17const getKeys = memoize((obj) => { 18 console.log('Computing keys...'); 19 return Object.keys(obj); 20}); 21 22const config = { a: 1, b: 2, c: 3 }; 23 24getKeys(config); // Computing keys... -> ['a', 'b', 'c'] 25getKeys(config); // Returns cached ['a', 'b', 'c']

Metadata Association#

1// Associate metadata without modifying objects 2const metadata = new WeakMap(); 3 4function setMeta(obj, key, value) { 5 if (!metadata.has(obj)) { 6 metadata.set(obj, new Map()); 7 } 8 metadata.get(obj).set(key, value); 9} 10 11function getMeta(obj, key) { 12 return metadata.get(obj)?.get(key); 13} 14 15function getAllMeta(obj) { 16 const meta = metadata.get(obj); 17 return meta ? Object.fromEntries(meta) : {}; 18} 19 20// Usage 21const user = { name: 'John' }; 22setMeta(user, 'created', Date.now()); 23setMeta(user, 'source', 'api'); 24 25console.log(getMeta(user, 'created')); 26console.log(getAllMeta(user)); // { created: ..., source: 'api' }

Comparison Table#

1/* 2Feature | WeakMap | WeakSet | Map | Set 3---------------------|----------------|----------------|----------------|---------------- 4Keys/Values | object → any | objects only | any → any | any 5Garbage Collection | Yes | Yes | No | No 6Iteration | No | No | Yes | Yes 7Size property | No | No | Yes | Yes 8Use case | Private data | Object flags | General cache | Unique values 9*/

When to Use#

1// Use WeakMap when: 2// 1. Storing private data for objects 3// 2. Caching based on object identity 4// 3. Associating data with DOM elements 5// 4. Preventing memory leaks with object keys 6 7// Use WeakSet when: 8// 1. Tracking object state (processed/visited) 9// 2. Instance validation 10// 3. Preventing duplicate operations 11// 4. Circular reference detection 12 13// Use regular Map/Set when: 14// 1. Need iteration 15// 2. Need size 16// 3. Need primitive keys 17// 4. Want to prevent garbage collection

Best Practices#

WeakMap: ✓ Private class data ✓ Object-keyed caches ✓ DOM element associations ✓ Metadata storage WeakSet: ✓ Object tracking flags ✓ Instance validation ✓ Visited/processed tracking ✓ Circular detection Memory: ✓ Let GC handle cleanup ✓ Don't hold other references ✓ Use for large object graphs ✓ Consider for event systems Avoid: ✗ Expecting iteration ✗ Using for primitives ✗ Relying on size ✗ Overcomplicating simple cases

Conclusion#

WeakMap and WeakSet are specialized collections for object-only keys that don't prevent garbage collection. Use WeakMap for private data, caching, and metadata association. Use WeakSet for tracking object state and validation. They're essential for preventing memory leaks in long-running applications, especially when working with DOM elements or large object graphs.

Share this article

Help spread the word about Bootspring