What are the actual uses of ES6 WeakMap?

What are the actual uses of ES6 WeakMap?

What are the actual uses of the WeakMap data structure introduced in ECMAScript 6?
Since a key of a weak map creates a strong reference to its corresponding value, ensuring that a value which has been inserted into a weak map will never disappear as long as its key is still alive, it can’t be used for memo tables, caches or anything else that you would normally use weak references, maps with weak values, etc. for.
It seems to me that this:
weakmap.set(key, value);

…is just a roundabout way of saying this:
key.value = value;

What concrete use cases am I missing?

Solutions/Answers:

Solution 1:

Fundamentally

WeakMaps provide a way to extend objects from the outside without interfering with garbage collection. Whenever you want to extend an object but can’t because it is sealed – or from an external source – a WeakMap can be applied.

A WeakMap is a map (dictionary) where the keys are weak – that is, if all references to the key are lost and there are no more references to the value – the value can be garbage collected. Let’s show this first through examples, then explain it a bit and finally finish with real use.

Let’s say I’m using an API that gives me a certain object:

var obj = getObjectFromLibrary();

Now, I have a method that uses the object:

function useObj(obj){
   doSomethingWith(obj);
}

I want to keep track of how many times the method was called with a certain object and report if it happens more than N times. Naively one would think to use a Map:

var map = new Map(); // maps can have object keys
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; // called one more time
    if(called > 10) report(); // Report called more than 10 times
    map.set(obj, called);
}

This works, but it has a memory leak – we now keep track of every single library object passed to the function which keeps the library objects from ever being garbage collected. Instead – we can use a WeakMap:

var map = new WeakMap(); // create a weak map
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; // called one more time
    if(called > 10) report(); // Report called more than 10 times
    map.set(obj, called);
}

And the memory leak is gone.

Use cases

Some use cases that would otherwise cause a memory leak and are enabled by WeakMaps include:

  • Keeping private data about a specific object and only giving access to it to people with a reference to the Map. A more ad-hoc approach is coming with the private-symbols proposal but that’s a long time from now.
  • Keeping data about library objects without changing them or incurring overhead.
  • Keeping data about a small set of objects where many objects of the type exist to not incur problems with hidden classes JS engines use for objects of the same type.
  • Keeping data about host objects like DOM nodes in the browser.
  • Adding a capability to an object from the outside (like the event emitter example in the other answer).
Related:  jquery - get text for element without children text

Let’s look at a real use

It can be used to extend an object from the outside. Let’s give a practical (adapted, sort of real – to make a point) example from the real world of Node.js.

Let’s say you’re Node.js and you have Promise objects – now you want to keep track of all the currently rejected promises – however, you do not want to keep them from being garbage collected in case no references exist to them.

Now, you don’t want to add properties to native objects for obvious reasons – so you’re stuck. If you keep references to the promises you’re causing a memory leak since no garbage collection can happen. If you don’t keep references then you can’t save additional information about individual promises. Any scheme that involves saving the ID of a promise inherently means you need a reference to it.

Enter WeakMaps

WeakMaps mean that the keys are weak. There are no ways to enumerate a weak map or to get all its values. In a weak map, you can store the data based on a key and when the key gets garbage collected so do the values.

This means that given a promise you can store state about it – and that object can still be garbage collected. Later on, if you get a reference to an object you can check if you have any state relating to it and report it.

This was used to implement unhandled rejection hooks by Petka Antonov as this:

process.on('unhandledRejection', function(reason, p) {
    console.log("Unhandled Rejection at: Promise ", p, " reason: ", reason);
    // application specific logging, throwing an error, or other logic here
});

We keep information about promises in a map and can know when a rejected promise was handled.

Solution 2:

This answer seems to be biased and unusable in a real world scenario. Please read it as is, and don’t consider it as an actual option for anything else than experimentation

A use case could be to use it as a dictionary for listeners, I have a coworker who did that. It is very helpful because any listener is directly targetted with this way of doing things. Goodbye listener.on.

But from a more abstract point of view, WeakMap is especially powerful to dematerialize access to basically anything, you don’t need a namespace to isolate its members since it is already implied by the nature of this structure. I’m pretty sure you could do some major memory improvements by replacing awkwards redundant object keys (even though deconstructing does the work for you).


Before reading what is next

I do now realize my emphasize is not exactly the best way to tackle the problem and as Benjamin Gruenbaum pointed out (check out his answer, if it’s not already above mine :p), this problem could not have been solved with a regular Map, since it would have leaked, thus the main strength of WeakMap is that it does not interfere with garbage collection given that they do not keep a reference.

Related:  How to get selected value of a dropdown menu in ReactJS

Here is the actual code of my coworker (thanks to him for sharing)

Full source here, it’s about listeners management I talked about above (you can also take a look at the specs)

var listenableMap = new WeakMap();


export function getListenable (object) {
    if (!listenableMap.has(object)) {
        listenableMap.set(object, {});
    }

    return listenableMap.get(object);
}


export function getListeners (object, identifier) {
    var listenable = getListenable(object);
    listenable[identifier] = listenable[identifier] || [];

    return listenable[identifier];
}


export function on (object, identifier, listener) {
    var listeners = getListeners(object, identifier);

    listeners.push(listener);
}


export function removeListener (object, identifier, listener) {
    var listeners = getListeners(object, identifier);

    var index = listeners.indexOf(listener);
    if(index !== -1) {
        listeners.splice(index, 1);
    }
}


export function emit (object, identifier, ...args) {
    var listeners = getListeners(object, identifier);

    for (var listener of listeners) {
        listener.apply(object, args);
    }
}

Solution 3:

WeakMap works well for encapsulation and information hiding

WeakMap is only available for ES6 and above. A WeakMap is a collection of key and value pairs where the key must be an object. In the following example, we build a WeakMap with two items:

var map = new WeakMap();
var pavloHero = {first: "Pavlo", last: "Hero"};
var gabrielFranco = {first: "Gabriel", last: "Franco"};
map.set(pavloHero, "This is Hero");
map.set(gabrielFranco, "This is Franco");
console.log(map.get(pavloHero));//This is Hero

We used the set() method to define an association between an object and another item (a string in our case). We used the get() method to retrieve the item associated with an object. The interesting aspect of the WeakMaps is the fact that it holds a weak reference to the key inside the map. A weak reference means that if the object is destroyed, the garbage collector will remove the entire entry from the WeakMap, thus freeing up memory.

var TheatreSeats = (function() {
  var priv = new WeakMap();
  var _ = function(instance) {
    return priv.get(instance);
  };

  return (function() {
      function TheatreSeatsConstructor() {
        var privateMembers = {
          seats: []
        };
        priv.set(this, privateMembers);
        this.maxSize = 10;
      }
      TheatreSeatsConstructor.prototype.placePerson = function(person) {
        _(this).seats.push(person);
      };
      TheatreSeatsConstructor.prototype.countOccupiedSeats = function() {
        return _(this).seats.length;
      };
      TheatreSeatsConstructor.prototype.isSoldOut = function() {
        return _(this).seats.length >= this.maxSize;
      };
      TheatreSeatsConstructor.prototype.countFreeSeats = function() {
        return this.maxSize - _(this).seats.length;
      };
      return TheatreSeatsConstructor;
    }());
})()

Solution 4:

I use WeakMap for the cache of worry-free memoization of functions that take in immutable objects as their parameter.

Memoization is fancy way of saying “after you compute the value, cache it so you don’t have to compute it again”.

Here’s an example:

// using immutable.js from here https://facebook.github.io/immutable-js/

const memo = new WeakMap();

let myObj = Immutable.Map({a: 5, b: 6});

function someLongComputeFunction (someImmutableObj) {
  // if we saved the value, then return it
  if (memo.has(someImmutableObj)) {
    console.log('used memo!');
    return memo.get(someImmutableObj);
  }
  
  // else compute, set, and return
  const computedValue = someImmutableObj.get('a') + someImmutableObj.get('b');
  memo.set(someImmutableObj, computedValue);
  console.log('computed value');
  return computedValue;
}


someLongComputeFunction(myObj);
someLongComputeFunction(myObj);
someLongComputeFunction(myObj);

// reassign
myObj = Immutable.Map({a: 7, b: 8});

someLongComputeFunction(myObj);
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>

A few things to note:

  • Immutable.js objects return new objects (with a new pointer) when you modify them so using them as keys in a WeakMap is guarantees the same computed value.
  • The WeakMap is great for memos because once the object (used as the key) gets garbage collected, so does the computed value on the WeakMap.
Related:  How to use React Router with Electron?

Solution 5:

Weak Maps can be used to store metadata about DOM elements without interfering with garbage collection or making coworkers mad at your code. For example, you could use them to numerical index all elements in a webpage.

Without WeakMaps or WeakSets:

var elements = document.getElementsByTagName('*'),
  i = -1, len = elements.length;

while (++i !== len) {
  // Production code written this poorly makes me want to cry:
  elements[i].lookupindex = i;
  elements[i].elementref = [];
  elements[i].elementref.push( elements[Math.pow(i, 2) % len] );
}

// Then, you can access the lookupindex's
// For those of you new to javascirpt, I hope the comments below help explain 
// how the ternary operator (?:) works like an inline if-statement
document.write(document.body.lookupindex + '<br />' + (
    (document.body.elementref.indexOf(document.currentScript) !== -1)
    ? // if(document.body.elementref.indexOf(document.currentScript) !== -1){
    "true"
    : // } else {
    "false"
  )   // }
);

Using WeakMaps and WeakSets:

var DOMref = new WeakMap(),
  __DOMref_value = Array,
  __DOMref_lookupindex = 0,
  __DOMref_otherelement = 1,
  elements = document.getElementsByTagName('*'),
  i = -1, len = elements.length, cur;

while (++i !== len) {
  // Production code written this greatly makes me want to 😊:
  cur = DOMref.get(elements[i]);
  if (cur === undefined)
    DOMref.set(elements[i], cur = new __DOMref_value)

  cur[__DOMref_lookupindex] = i;
  cur[__DOMref_otherelement] = new WeakSet();
  cur[__DOMref_otherelement].add( elements[Math.pow(i, 2) % len] );
}

// Then, you can access the lookupindex's
cur = DOMref.get(document.body)
document.write(cur[__DOMref_lookupindex] + '<br />' + (
    cur[__DOMref_otherelement].has(document.currentScript)
    ? // if(cur[__DOMref_otherelement].has(document.currentScript)){
    "true"
    : // } else {
    "false"
  )   // }
);

The Difference

The difference may look negligible, aside from the fact that the weakmap version is longer, however there is a major difference between the two pieces of code shown above. In the first snippet of code, without weak maps, the piece of code stores references every which way between the DOM elements. This prevents the DOM elements from being garbage collected. Math.pow(i, 2) % len] may seem like an oddball that nobody would use, but think again: plenty of production code has DOM references that bounce all over the document. Now, for the second piece of code, because all the references to the elements are weak, when you a remove a node, the browser is able to determine that the node is not used (not able to be reached by your code), and thus delete it from memory. The reason for why you should be concerned about memory usage, and memory anchors (things like the first snippet of code where unused elements are held in memory) is because more memory usage means more browser GC-attempts (to try to free up memory to avert a browser crash) means slower browsing experience and sometimes a browser crash.

As for a polyfill for these, I would recommend my own library (found here @ github). It is a very lightweight library that will simply polyfill it without any of the way-overly-complex frameworks you might find in other polyfills.

~ Happy coding!