ES6 Note: Weakmap

    22 Aug, 2025

    The introduction and usage examples for Weakmap in ES6, with comparing to the Map.

    The WeakMap is a special type of collection in JavaScript. It was formally introduced in the ES6 standard and is similar to Map, but with several key differences:

    • A WeakMap is a collection of key-value pairs (key → value);
    • Keys can only be objects (plain objects, arrays, functions, etc.), not primitives (numbers, strings, booleans, etc.);
    • WeakMap holds weak references to its keys, meaning: If no other variables reference the object anymore, the garbage collector will automatically reclaim it, and the corresponding entry in the WeakMap will disappear.
    FeatureMapWeakMap
    Key TypesAny value (objects or primitives)Objects only
    Key ReferenceStrong referenceWeak reference
    EnumerableEnumerable (iterable via map.keys(), map.entries())Not enumerable (no iteration methods)
    Use CasesGeneral key-value mapping storageData that needs to be tied to an object's lifecycle

    Why are weak references needed? Suppose we want to attach "extra information" to certain objects but don't want this information to prevent the objects from being garbage collected. For example:

    const wm = new WeakMap();
    
    function createUser(name) {
      const user = { name };
      wm.set(user, { created: Date.now() });
      return user;
    }
    
    let alice = createUser("Alice");
    console.log(wm.get(alice));
    // { created: 1692624526805 }  // e.g., stores creation time
    
    // When alice is no longer referenced, the object will be GC'd, and the entry in the WeakMap disappears
    alice = null;
    

    Here, the role of WeakMap is to store additional metadata without worrying about the object "living forever" because it's stored in a Map.

    WeakMap acts like an Object Additional Information Warehouse —the keys inside it do not affect object reclamation. Its greatest value is: following the object's lifecycle and avoiding memory leaks.

    Many people mistakenly think that the "weak" reference in WeakMap is implemented via reference counting, but that's not the case.

    Reference counting is a garbage collection algorithm: an object's reference count drops to zero → memory is released. However, modern JavaScript engines (V8, SpiderMonkey, etc.) do not rely solely on reference counting. Instead, they use garbage collection centered around reachability analysis. Therefore, the "weak reference" in WeakMap is not "delete when reference count = 0". Rather: The key in a WeakMap is not considered a "root of reachability".

    In a regular Map, the key object is strongly referenced. Even if no external variables reference it anymore, it remains "reachable" and won't be GC'd.

    In a WeakMap, the key is a weak reference. The garbage collector does not count it as part of the "live objects": if there are no other strong references from the outside, the key will be reclaimed by GC, and the corresponding key → value mapping will disappear.

    Because the JS engine needs the freedom to remove entries during GC, if it allowed you to enumerate the WeakMap, you might see entries that "have been reclaimed but not yet cleaned up," making the semantics uncertain. Therefore, WeakMap only supports set/get/has/delete and cannot be iterated.

    The underlying mechanism of WeakMap is not "reference counting" but is based on the reachability-based garbage collection mechanism. Its "weak reference" essentially tells the GC: Keys in the WeakMap do not count as active references. Thus, WeakMap is very suitable for storing "additional data that disappears along with the object."

    Below is a diagram illustrating the difference in garbage collection between a regular Map and a WeakMap.

    For a strongly referencing Map:

    let map = new Map();
    let obj = { name: "Alice" };
    
    map.set(obj, "metadata");
    
    // Now, even if obj = null,
    // the map still "strongly references" this object
    obj = null;
    

    The memory relationship is:

    [Global variable map] ──► [Map instance]
                                   │
                                   ▼
                             ┌───────────────┐
                             │ key: {Alice}  │──► "metadata"
                             └───────────────┘
    

    Even after obj = null, the Map still holds {Alice}, which won't be GC'd. This can easily cause memory leaks.

    For a weakly referencing WeakMap:

    let wm = new WeakMap();
    let obj = { name: "Alice" };
    
    wm.set(obj, "metadata");
    
    // Remove the external reference to obj
    obj = null;
    

    Its memory relationship is as follows:

    [Global variable wm] ──► [WeakMap instance]
                                     │
                                     ▼
                               ┌───────────────┐
                               │ key: {Alice}  │──► "metadata"
                               └───────────────┘
    

    After obj = null, there are no strong references pointing to {Alice}: GC considers {Alice} "unreachable," reclaims the object, and the key-value pair in the WeakMap automatically disappears.

    This does not prevent garbage collection.

    In Node.js, directly observing garbage collection is difficult because GC is automatic. However, starting from Node.js v14, WeakRef and FinalizationRegistry are available, allowing us to simulate the WeakMap reclamation process.

    Here is some experimental code:

    // Save a WeakMap
    const wm = new WeakMap();
    
    // Register a FinalizationRegistry to notify when an object is reclaimed
    const registry = new FinalizationRegistry((heldValue) => {
      console.log(`Object has been reclaimed, the corresponding metadata was: ${heldValue}`);
    });
    
    (function test() {
      let obj = { name: "Alice" };
    
      // Store a record in the WeakMap
      wm.set(obj, "metadata for Alice");
    
      // Register a reclamation callback for the object
      registry.register(obj, "metadata for Alice");
    
      console.log("Object created and stored in WeakMap");
    
      // Manually clear the strong reference to obj
      obj = null;
    })();
    
    // Prompt Node.js to perform garbage collection
    // ⚠️ Requires running with the flag: node --expose-gc test.js
    setTimeout(() => {
      console.log("Attempting to manually trigger garbage collection");
      if (global.gc) {
        global.gc();
      } else {
        console.log("Please run this script with node --expose-gc");
      }
    }, 1000);
    

    Next, save the code as test.js and run it with the GC flag:

    node --expose-gc test.js
    

    You will see output similar to:

    Object created and stored in WeakMap
    Attempting to manually trigger garbage collection
    Object has been reclaimed, the corresponding metadata was: metadata for Alice
    

    Thus, we can see:

    • The key in WeakMap does not prevent object reclamation;
    • FinalizationRegistry can trigger a callback after the object is GC'd, proving the WeakMap entry has disappeared;
    • This shows the essence of WeakMap's "weak reference" is: It does not treat the key as a root of reachability for GC.

    Below are several typical use cases for WeakMap. You'll find it's primarily used for "attaching extra information to objects without wanting to affect the object's lifecycle":

    1. Private properties (security + automatic release)
    2. Caching (automatically cleaned up when the object is released)
    3. DOM/object metadata storage (without polluting the original object)
    4. Event listener management
    5. Implicit information attachment inside libraries

    Private property storage (simulating class private fields)

    const privateData = new WeakMap();
    
    class User {
      constructor(name, password) {
        this.name = name;
        privateData.set(this, { password });
      }
    
      checkPassword(pw) {
        return privateData.get(this).password === pw;
      }
    }
    
    const u = new User("Alice", "123456");
    console.log(u.checkPassword("123456")); // true
    console.log(u.password); // undefined (cannot access directly)
    

    Using WeakMap to save "private data"; when the object instance is reclaimed, the data is automatically released too.

    Caching (Memoization)

    const cache = new WeakMap();
    
    function heavyCompute(obj) {
      if (cache.has(obj)) {
        return cache.get(obj);
      }
      let result = expensiveCalculation(obj);
      cache.set(obj, result);
      return result;
    }
    
    function expensiveCalculation(o) {
      return JSON.stringify(o).length; // Assume it's computationally expensive
    }
    
    let data = { foo: "bar" };
    console.log(heavyCompute(data)); // Compute and cache
    console.log(heavyCompute(data)); // Read directly from cache
    data = null; // Release the object, cache automatically cleans up
    

    Ideal for "object → result" caching scenarios, prevents memory leaks.

    DOM metadata storage

    const elementData = new WeakMap();
    
    function bindInfo(el, info) {
      elementData.set(el, info);
    }
    
    function getInfo(el) {
      return elementData.get(el);
    }
    
    // Usage
    const button = document.querySelector("button");
    bindInfo(button, { clicked: 0 });
    
    button.addEventListener("click", () => {
      const info = getInfo(button);
      info.clicked++;
      console.log(`Button click count: ${info.clicked}`);
    });
    

    When the button node is removed from the DOM and becomes unreachable, the WeakMap automatically clears the metadata, preventing memory leaks.

    Event listener state tracking

    const listenerMap = new WeakMap();
    
    function trackListener(obj, listener) {
      obj.addEventListener("click", listener);
      listenerMap.set(obj, listener);
    }
    
    function removeListener(obj) {
      const listener = listenerMap.get(obj);
      obj.removeEventListener("click", listener);
      listenerMap.delete(obj);
    }
    

    Tracks bound listeners and automatically releases them.

    Cross-library/cross-module additional information

    If you are writing a library and don't want to pollute user objects (avoid adding properties directly), WeakMap can "attach" data externally:

    // myLib.js
    const meta = new WeakMap();
    
    export function attach(obj, info) {
      meta.set(obj, info);
    }
    
    export function getInfo(obj) {
      return meta.get(obj);
    }
    

    Users won't see extra properties, and there's no risk of memory leaks.