See Testim Mobile in action. Live demo with Q&A on Mar 28 | Save your spot

WeakMap Weakness

ES2015 (ES6) added several new data structures. While Map and Set are more straightforward, ES2015 also added "weak collections" like…

Testim
By Testim,

ES2015 (ES6) added several new data structures. While Map and Set are more straightforward, ES2015 also added “weak collections” like WeakMap and WeakSet.

In this post we’ll go over why maps were added, what’s “weak” about a WeakMap and when one might want to use it.

What we did before maps

JavaScript has had object literals forever. These allow us to save mapping between keys and values and are extensively used in code in practice:

const o = {};
o['hello'] = 'world'; // store property 

// later
o['hello']; // returns world

This works because JavaScript objects are key-value stores. While JavaScript engines don’t actually store objects as key-value maps in practice (usually) JavaScript lets us patch properties on objects just fine in practice and this pattern is quite common.

Bracket notation allows us to access a key by “dynamic” value which enables this:

const o = {};

function set(key, value) {
  o[key] = value;
}
function get(key) {
  return o[key];
}
set("hello", "world");
get("hello"); // returns "world"

What went wrong?

Objects in JavaScript only have string keys (well, and symbols since ES2015, and private fields since ES2019, let’s ignore those). Every time an object key is accessed it is converted to a string if such conversion is possible:

const o = {};
o[5] = 10;
o["5"]; // 10;
o[5n]; // 10;

This also means that we can’t store object keys meaningfully:

o[{x:3}] = 5;
o[{y:5}]; // returns 5! This is because both are coerced to "[object Object]"

Maps let us store non-string keys in objects.

A map is created with the Map constructor and the properties of Map can be anything:

const o = new Map();
o.set(5, 5);
o.get("5"); // undefined since 5 is not "5";
const foo = {};
o.set(foo, 10);
o.get(foo); // 10
o.get({}); // undefined, since object equality is by reference

This means that maps take object keys, unlike objects. Sadly, we can’t define the hash function in a built-in map – but we might one day.

Why weak?

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

Should I use a WeakMap?

In general, I would recommend using a WeakMap only after considering other alternatives since they are inherently less visible than regular maps and objects (for example, they can’t be iterated).

However, when you extend an object from the outside (which isn’t uncommon) – it’s worth considering.

Author bio: This post was written by Benjamin Gruenbaum. Benji is an open source lover, Node.js core collaborator, core team at Bluebird, Sinon, MobX and other open-source libraries.