Skip to content

WeakMap and WeakSet: Memory-Friendly Weak Reference Collections

Imagine a library's borrowing record system—when a book is removed from the shelves, related borrowing history should also be automatically cleared rather than permanently occupying storage space. JavaScript's WeakMap and WeakSet provide such an "automatic cleanup" mechanism—they use weak references to store data, and when objects are no longer used elsewhere, this data is automatically garbage collected. Although their functionality is limited compared to Map and Set, in specific scenarios they are powerful tools for preventing memory leaks.

Understanding Weak References

Before diving into WeakMap and WeakSet, we need to understand the concept of "weak references."

Strong Reference vs Weak Reference

javascript
// Strong reference: ordinary object reference
let user = { name: "Alice", age: 25 };
let users = [user]; // Array holds strong reference to user

// Even if user variable is set to null, object still exists
user = null;
console.log(users[0]); // { name: "Alice", age: 25 } - object still in memory

// Weak reference: WeakMap/WeakSet references
let weakMap = new WeakMap();
let obj = { id: 1 };
weakMap.set(obj, "some data");

// When obj has no other references, it can be garbage collected
// WeakMap entries are also automatically deleted
obj = null; // Object can now be reclaimed (assuming no other references)

Strong references are like pinning a photo to a wall with a nail—as long as the nail is still there, the photo won't fall off. Weak references are like sticking a photo to a wall with a magnet—once the magnetism disappears (no other strong references), the photo will automatically fall off.

WeakSet - Weak Reference Object Collection

WeakSet is similar to Set but can only store objects and holds weak references to these objects.

Basic Features

javascript
let weakSet = new WeakSet();

// ✅ Can add objects
let obj1 = { name: "Object 1" };
let obj2 = { name: "Object 2" };

weakSet.add(obj1);
weakSet.add(obj2);

// ❌ Cannot add basic type values
// weakSet.add(1);        // TypeError
// weakSet.add("string"); // TypeError
// weakSet.add(true);     // TypeError

// Check if object exists
console.log(weakSet.has(obj1)); // true

// Delete object
weakSet.delete(obj1);
console.log(weakSet.has(obj1)); // false

WeakSet only has three methods: add(), has(), and delete(). There's no size property, and it cannot be iterated because the nature of weak references makes it impossible to determine how many elements are in the collection (they can be reclaimed at any time).

Garbage Collection Demonstration

javascript
let weakSet = new WeakSet();

// Create object and add to WeakSet
let obj = { data: "important" };
weakSet.add(obj);

console.log(weakSet.has(obj)); // true

// Remove strong reference
obj = null;

// At this point, object has no other strong references and can be garbage collected
// WeakSet entries are also automatically deleted
// (Actual collection time is determined by garbage collector, not immediate)

// We cannot directly verify if object was reclaimed,
// because without obj reference, we cannot call weakSet.has(obj)

Practical Application: Marking DOM Elements

javascript
class DOMElementTracker {
  constructor() {
    this.processedElements = new WeakSet();
  }

  // Process DOM element
  process(element) {
    if (this.processedElements.has(element)) {
      console.log("Element already processed");
      return;
    }

    // Execute processing logic
    console.log("Processing element...");
    element.classList.add("processed");

    // Mark as processed
    this.processedElements.add(element);
  }

  // Check if element is processed
  isProcessed(element) {
    return this.processedElements.has(element);
  }
}

// Usage example (browser environment)
// let tracker = new DOMElementTracker();
// let button = document.querySelector('.btn');
// tracker.process(button);
// tracker.process(button); // "Element already processed"

// When DOM element is removed from page and has no other references,
// WeakSet entries are automatically cleaned up, preventing memory leaks

WeakMap - Weak Reference Key-Value Collection

WeakMap is similar to Map but keys must be objects, and it holds weak references to the keys.

Basic Features

javascript
let weakMap = new WeakMap();

let key1 = { id: 1 };
let key2 = { id: 2 };

// Set key-value pairs
weakMap.set(key1, "value 1");
weakMap.set(key2, "value 2");

// ❌ Keys must be objects
// weakMap.set("string", "value"); // TypeError
// weakMap.set(123, "value");      // TypeError

// Get value
console.log(weakMap.get(key1)); // "value 1"

// Check if key exists
console.log(weakMap.has(key2)); // true

// Delete key-value pair
weakMap.delete(key1);
console.log(weakMap.has(key1)); // false

WeakMap also only has four methods: set(), get(), has(), and delete(). There's no size, keys(), values(), or entries(), and it cannot be iterated.

Garbage Collection Mechanism

javascript
let weakMap = new WeakMap();

function createUser() {
  let user = { name: "Bob", email: "[email protected]" };
  weakMap.set(user, { lastLogin: new Date(), sessionId: "abc123" });
  return user;
}

let user = createUser();
console.log(weakMap.get(user)); // { lastLogin: ..., sessionId: "abc123" }

// Remove strong reference
user = null;

// Now user object can be garbage collected
// Corresponding WeakMap entry is also automatically deleted
// No memory leak

Practical Application: Private Data Storage

A classic use of WeakMap is storing private data for objects:

javascript
const privateData = new WeakMap();

class User {
  constructor(name, email, password) {
    // Public data
    this.name = name;
    this.email = email;

    // Private data stored in WeakMap
    privateData.set(this, {
      password: password,
      createdAt: new Date(),
    });
  }

  // Verify password (access private data)
  authenticate(password) {
    let data = privateData.get(this);
    return data.password === password;
  }

  // Change password
  changePassword(oldPassword, newPassword) {
    if (!this.authenticate(oldPassword)) {
      throw new Error("Invalid old password");
    }

    let data = privateData.get(this);
    data.password = newPassword;
  }

  // Get account age
  getAccountAge() {
    let data = privateData.get(this);
    let now = new Date();
    return Math.floor((now - data.createdAt) / (1000 * 60 * 60 * 24));
  }
}

// Usage example
let user = new User("Emma", "[email protected]", "secret123");

console.log(user.name); // "Emma" - public property accessible
console.log(user.password); // undefined - password is private

console.log(user.authenticate("secret123")); // true
console.log(user.authenticate("wrong")); // false

user.changePassword("secret123", "newPassword456");
console.log(user.authenticate("newPassword456")); // true

// When user object is destroyed, WeakMap private data is also reclaimed

Practical Application: Caching Computation Results

javascript
class ExpensiveCalculator {
  constructor() {
    this.cache = new WeakMap();
  }

  // Complex calculation (example: calculate object hash)
  calculate(obj) {
    // Check cache
    if (this.cache.has(obj)) {
      console.log("Returning cached result");
      return this.cache.get(obj);
    }

    // Execute calculation
    console.log("Performing calculation...");
    let result = this.performExpensiveOperation(obj);

    // Cache result
    this.cache.set(obj, result);

    return result;
  }

  performExpensiveOperation(obj) {
    // Simulate complex calculation
    let hash = 0;
    let str = JSON.stringify(obj);
    for (let i = 0; i < str.length; i++) {
      hash = (hash << 5) - hash + str.charCodeAt(i);
      hash = hash & hash;
    }
    return hash;
  }
}

// Usage example
let calculator = new ExpensiveCalculator();

let data1 = { id: 1, values: [1, 2, 3] };
let data2 = { id: 2, values: [4, 5, 6] };

console.log(calculator.calculate(data1)); // Performing calculation...
console.log(calculator.calculate(data1)); // Returning cached result

console.log(calculator.calculate(data2)); // Performing calculation...

// When data1 and data2 are no longer used, cache is automatically cleaned up
data1 = null; // Cache entries can be reclaimed

Practical Application: Associating Metadata

javascript
class ElementMetadata {
  constructor() {
    this.metadata = new WeakMap();
  }

  // Set metadata for element
  setMetadata(element, data) {
    if (!this.metadata.has(element)) {
      this.metadata.set(element, {});
    }

    let meta = this.metadata.get(element);
    Object.assign(meta, data);
  }

  // Get metadata
  getMetadata(element, key) {
    if (!this.metadata.has(element)) {
      return undefined;
    }

    let meta = this.metadata.get(element);
    return key ? meta[key] : meta;
  }

  // Increment counter
  incrementCounter(element, counterName) {
    if (!this.metadata.has(element)) {
      this.metadata.set(element, {});
    }

    let meta = this.metadata.get(element);
    meta[counterName] = (meta[counterName] || 0) + 1;
    return meta[counterName];
  }
}

// Usage example (browser environment)
// let metadataManager = new ElementMetadata();
//
// let button = document.querySelector('.btn');
// metadataManager.setMetadata(button, {
//   tooltip: "Click to submit",
//   theme: "primary"
// });
//
// // Track click count
// button.addEventListener('click', () => {
//   let clicks = metadataManager.incrementCounter(button, 'clicks');
//   console.log(`Button clicked ${clicks} times`);
// });
//
// // When button is removed from DOM and has no other references, metadata is automatically cleaned up

WeakMap vs Map Comparison

Let's systematically compare the differences between WeakMap and Map:

javascript
// 1. Key types
let map = new Map();
let weakMap = new WeakMap();

// Map can use any type as key
map.set("string", "value");
map.set(123, "value");
map.set({ id: 1 }, "value");

// WeakMap can only use objects as keys
let objKey = { id: 1 };
weakMap.set(objKey, "value");
// weakMap.set("string", "value"); // TypeError

// 2. Reference types
// Map holds strong references
let obj1 = { name: "Test" };
map.set(obj1, "data");
obj1 = null; // Object still in Map, won't be reclaimed

// WeakMap holds weak references
let obj2 = { name: "Test" };
weakMap.set(obj2, "data");
obj2 = null; // Object can be garbage collected

// 3. Iterability
console.log(map.size); // Can get size
for (let [key, value] of map) {
  // Can iterate
  console.log(key, value);
}

// console.log(weakMap.size);     // undefined - no size
// for (let entry of weakMap) {}  // TypeError - not iterable

// 4. Available methods
// Map: set, get, has, delete, clear, keys, values, entries, forEach
// WeakMap: set, get, has, delete (only these four)

Choosing WeakMap or Map?

javascript
// ✅ Use WeakMap when:
// 1. Need to associate objects with data but don't want to affect garbage collection
let elementCache = new WeakMap();

// 2. Store private data
const privateProps = new WeakMap();

// 3. Temporary cache, don't need manual cleanup
let computationCache = new WeakMap();

// ❌ Don't use WeakMap when:
// 1. Need to iterate key-value pairs
// 2. Need to know collection size
// 3. Keys are not objects
// 4. Need to serialize data

// ✅ Use Map when:
// 1. Need to iterate or get all keys/values
let userRoles = new Map();

// 2. Need to persist data
let appConfig = new Map();

// 3. Need to use non-object keys
let statusCodes = new Map([
  [200, "OK"],
  [404, "Not Found"],
]);

Real-world Case: Observer Pattern

Let's implement a memory-safe observer pattern using WeakMap:

javascript
class EventEmitter {
  constructor() {
    // Use WeakMap to store listeners, avoiding memory leaks
    this.listeners = new WeakMap();
  }

  // Register event listener for object
  on(target, eventName, callback) {
    if (!this.listeners.has(target)) {
      this.listeners.set(target, new Map());
    }

    let targetListeners = this.listeners.get(target);

    if (!targetListeners.has(eventName)) {
      targetListeners.set(eventName, []);
    }

    targetListeners.get(eventName).push(callback);
  }

  // Remove event listener
  off(target, eventName, callback) {
    if (!this.listeners.has(target)) {
      return;
    }

    let targetListeners = this.listeners.get(target);

    if (!targetListeners.has(eventName)) {
      return;
    }

    let callbacks = targetListeners.get(eventName);
    let index = callbacks.indexOf(callback);

    if (index > -1) {
      callbacks.splice(index, 1);
    }
  }

  // Trigger event
  emit(target, eventName, ...args) {
    if (!this.listeners.has(target)) {
      return;
    }

    let targetListeners = this.listeners.get(target);

    if (!targetListeners.has(eventName)) {
      return;
    }

    let callbacks = targetListeners.get(eventName);
    callbacks.forEach((callback) => {
      callback.apply(target, args);
    });
  }

  // Remove all listeners for object
  removeAllListeners(target) {
    this.listeners.delete(target);
  }
}

// Usage example
let emitter = new EventEmitter();

class DataModel {
  constructor(name) {
    this.name = name;
    this.data = {};
  }

  set(key, value) {
    this.data[key] = value;
    emitter.emit(this, "change", key, value);
  }

  get(key) {
    return this.data[key];
  }
}

// Create model
let userModel = new DataModel("user");

// Register listener
let onChange = (key, value) => {
  console.log(`User data changed: ${key} = ${value}`);
};

emitter.on(userModel, "change", onChange);

// Trigger events
userModel.set("name", "Alice"); // User data changed: name = Alice
userModel.set("age", 25); // User data changed: age = 25

// When userModel is destroyed, WeakMap listeners are automatically cleaned up
userModel = null; // All related listeners can be reclaimed

Memory Leak Comparison

Let's look at an actual memory leak scenario and solution:

javascript
// ❌ Problem: Using Map can cause memory leaks
class LeakyCache {
  constructor() {
    this.cache = new Map();
  }

  processElement(element) {
    // Cache processing result for each element
    let result = this.performProcessing(element);
    this.cache.set(element, result);
    return result;
  }

  performProcessing(element) {
    // Simulate complex processing
    return { processed: true, timestamp: Date.now() };
  }
}

// Problem: Even when element is removed from DOM, Map still holds reference
// let cache = new LeakyCache();
// let div = document.createElement('div');
// cache.processElement(div);
// div.remove(); // Removed from DOM
// div = null;   // But Map still holds reference, causing memory leak!

// ✅ Solution: Use WeakMap
class SafeCache {
  constructor() {
    this.cache = new WeakMap();
  }

  processElement(element) {
    // Check cache
    if (this.cache.has(element)) {
      return this.cache.get(element);
    }

    // Process and cache
    let result = this.performProcessing(element);
    this.cache.set(element, result);
    return result;
  }

  performProcessing(element) {
    return { processed: true, timestamp: Date.now() };
  }
}

// WeakMap automatically cleans up when element is removed and has no other references
// let safeCache = new SafeCache();
// let div = document.createElement('div');
// safeCache.processElement(div);
// div.remove();
// div = null; // WeakMap entries can be reclaimed, no leak

Limitations and Considerations

1. Not Serializable

javascript
let weakMap = new WeakMap();
let obj = { id: 1 };
weakMap.set(obj, "data");

// ❌ Cannot serialize WeakMap
console.log(JSON.stringify(weakMap)); // "{}"

// If serialization is needed, use Map
let map = new Map([[obj, "data"]]);
let serialized = JSON.stringify([...map]);

2. Cannot Get Size or Iterate

javascript
let weakSet = new WeakSet();
let obj1 = { id: 1 };
let obj2 = { id: 2 };
weakSet.add(obj1);
weakSet.add(obj2);

// ❌ Cannot get size
// console.log(weakSet.size); // undefined

// ❌ Cannot iterate
// for (let obj of weakSet) {} // TypeError

// This is by design because weakly referenced objects can be reclaimed at any time

3. Keys Must Be Objects

javascript
let weakMap = new WeakMap();

// ❌ Basic types cannot be keys
// weakMap.set(1, "value");      // TypeError
// weakMap.set("key", "value");  // TypeError
// weakMap.set(Symbol(), "val"); // TypeError

// ✅ Can only use objects
weakMap.set({}, "value");
weakMap.set([], "value");
weakMap.set(new Date(), "value");

Performance Considerations

javascript
// WeakMap/WeakMap perform better in some scenarios because no manual memory management needed

let size = 10000;

// Test: Map needs manual cleanup
console.time("Map with manual cleanup");
let map = new Map();
let objects = [];

for (let i = 0; i < size; i++) {
  let obj = { id: i };
  objects.push(obj);
  map.set(obj, `data${i}`);
}

// Cleanup: need to manually delete
objects.forEach((obj) => map.delete(obj));
console.timeEnd("Map with manual cleanup");

// Test: WeakMap auto cleanup (but we can't directly test reclamation)
console.time("WeakMap auto cleanup");
let weakMap = new WeakMap();

for (let i = 0; i < size; i++) {
  let obj = { id: i };
  weakMap.set(obj, `data${i}`);
  // obj has no reference after loop ends, can be reclaimed
}
console.timeEnd("WeakMap auto cleanup");

Summary

WeakMap and WeakSet are special collection types in JavaScript:

WeakSet characteristics:

  • Only stores objects
  • Holds weak references to objects
  • Not iterable, no size
  • Methods: add, has, delete

WeakMap characteristics:

  • Keys must be objects
  • Holds weak references to keys
  • Not iterable, no size
  • Methods: set, get, has, delete

Main advantages:

  • Automatic garbage collection, preventing memory leaks
  • Suitable for storing temporary data or metadata
  • Doesn't affect object lifecycle

Applicable scenarios:

  • Store DOM element associated data
  • Implement private properties
  • Cache object computation results
  • Mark/track object state

Not applicable scenarios:

  • Need to iterate or get size
  • Need serialization
  • Need to use basic types as keys
  • Need to persist data

Although WeakMap and WeakSet have limited functionality, they are indispensable tools in specific scenarios. Using them correctly can make your application more memory-efficient and avoid common memory leak problems. Understanding when to use weak reference collections is an important step toward becoming an advanced JavaScript developer.