JavaScript maps and sets part 2: Weak maps

JavaScript maps and sets part 2: Weak maps

1 Jan 2021 weak-maps maps javascript

Welcome to part 2 of my in-depth guide to JavaScript maps and sets. In this part, we'll be looking at weak maps, which are altogether more exotic than normal maps.

In this part we'll be looking only at features unique to weak maps; to learn about maps in general, see part 1: maps.

What are weak maps? 🔗

Weak maps are maps (duh), but with a couple of differences. We learned in part 1 that, with maps, keys can be of any data type, unlike with traditional objects, where keys are strings or symbols.

This means we can even use objects as keys. However, with weak maps, the keys are always objects. They can't be anything else.

let map = new WeakMap(); map.set('foo', 'bar');

That will throw an error:

Uncaught TypeError: WeakMap key must be an object, got "foo"

Why must keys be objects in weak maps? The reason is intrinsically linked to the other important differences between weak maps and maps.

Weakly referenced keys 🔗

What the hell does that mean? Well, it has to do with JavaScript garbage collection.

Now, one of the joys of JavaScript is that garbage collection - i.e. handling memory and destroying unused objects - is handled automatically. This contrasts with languages such as C, where the developer manages this manually.

A quick primer on JS garbage collection 🔗

One of the joys of JavaScript is we don't have to worry about garbage collection - it's handled automatically for us. Contrast this with languages such as C, where garbage collection and memory management are handled explicitly by the developer.

In JavaScript, garbage collection happens on an object when there are no further references to it.

let myobj = {foo: 'bar'}; myobj = null; //overwrite the object to schedule garbage collection

If there's lingering references, though, garbage collection won't happen for the object. How could it? The reference still needs access to it.

let myobj = {foo: 'bar'}; let myref = myobj; myobj = null; myref; //{foo: 'bar'} - object still alive and well, even though @myobj is dead

One thing that's important to understand is we, the developer, can't be certain of the exact time when garbage collection happens. This is up to the JavaScript engine; it may happen immediately, or it may schedule it for a short while later.

Weak maps and garbage collection 🔗

Now, what does this have to do with weak maps?

When an object is stored as a key in a weak map, it is automatically garbage collected any time when, in the future, no references to that object persist. By implication, this means it's also removed from the map (since, if an object is garbage collected, it can no longer exist anywhere, including in our map).

let mymap = new WeakMap(); let myobj = {foo: 'bar'}; mymap.set(myobj, 'some data'); mymap.has(myobj); //true myobj = null; //overwrite object - garbage-collected and removed from map!

In other words, weak maps do not stand in the way of garbage collection in the way that objects stored as keys in normal maps (or as values in arrays, etc) would do.

Note that console.log()'ing a weak map after references to an object key have been deleted/overwritten, may still show the key in the map. Consoles sometimes play by their own rules.

As a result, the contents of weak map exist in a sort of quantum state. Some parts may genuinely exist, some parts may be earmarked for garbage collection and removal, some parts may have already been garbage collected and removed.

This is why weak map keys must be objects - in JavaScript there's no such thing as references to primitive (non-object) values; primitives are copied by value, not reference.

Non-enumerability 🔗

Weak maps are not enumerable. This means the enumeration methods of the map API, e.g. .entries(), are not available.

let mymap = new WeakMap(); mymap.set(null, 'bar'); mymap.entries(); //error - mymap.entries() is not a function

When we think about it, weak maps couldn't be enumerable; if the keys in them could disappear at any time (due to garbage collection), and garbage collection doesn't happen at a precise, knowable time, then we could never be sure what the content of the weak map was.

To retrieve an entry from a weak map, we must have a reference to its key and use .get(key) - there's no other way to peer inside and see what's in there.

Use cases 🔗

Private data 🔗

Let's look at a real-life use case for weak maps. In this example the impetus is to store private data against an object, keeping the private and public data separate.

const map = new WeakMap(); class Book { constructor(title) { this.title = title; const secret = { rating: 'rubbish!' } map.set(this, secret); } } let book = new Book('A Terrible Book');

There, we want to keep our thoughts on the book to ourselves, not expose it in the public instance, book. Our weak map does that; it associates an object of private data against the instance, but separate from it, so the consumer of book never sees it.

Efficient cache 🔗

Let's see another user case. It's common to store the results of computation in an object cache.

You'll be familiar with this if you've used something like Vue.js' computed properties.

With weak maps, we can do this, with the added advantage that data is automatically removed from our cache (the weak map) when the object it relates to ceases to be.

let cache = new WeakMap(); function ageSquare(person) { if (!cache.has(person)) cache.set(person, person.age **2; return cache.get(person); } let person = {name: 'Bob', age: 93}; ageSquare(person); //freshly-computed result - logged in cache ageSquare(person); //result served from cache

Such a system is efficient because, if person is ever overwritten with null, its cache entry will be automatically removed from the cache (map).

API 🔗

Weak maps have a similar API to that of maps - except, as discussed above, they lack enumeration methods and also have no .size property. The full API is, therefore:

  • new WeakMap([entries]) - constructor - create a new weak map
  • get(key) - get a specific key's value from the map
  • set(key, val) - set a key/value in the map - returns the map, so you can chain multiple calls to set()
  • has(key) - returns boolean denoting whether the map contains an entry denoted by the passed key
  • delete(key) - delete a specific entry in the map, by key

The "quantum state" of weak maps means you can't ever casually browse their contents; to retrieve an item, you need a reference to the key (object).

Summary 🔗

Weak maps are like maps, but with a few key differences. Firstly, keys must be objects, whereas with normal maps keys merely can be objects.

Secondly, objects stored as keys in weak maps do not prevent garbage collection. Once there are no remaining references to the object outside the map, the object is garbage-collected and, by implication, removed from the map.

Because of the uncertain state of weak maps at any given time, enumeration is not possible. Instead, to retrieve something from a weak map, you must have a reference to its key.

---

Up next, in the final part of this guide, sets and weak sets!