ponyfoo.com

ES6 WeakMaps, Sets, and WeakSets in Depth

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

Welcome once again to ES6 – “I can’t take this anymore” – in Depth. New here? Start with A Brief History of ES6 Tooling. Then, make your way through destructuring, template literals, arrow functions, the spread operator and rest parameters, improvements coming to object literals, the new classes sugar on top of prototypes, let, const, and the “Temporal Dead Zone”, iterators, generators, Symbols and Maps. This morning we’ll be discussing three more collection data structures coming in ES6: WeakMap, Set and WeakSet.

Like I did in previous articles on the series, I would love to point out that you should probably set up Babel and follow along the examples with either a REPL or the babel-node CLI and a file. That’ll make it so much easier for you to internalize the concepts discussed in the series. If you aren’t the “install things on my computer” kind of human, you might prefer to hop on CodePen and then click on the gear icon for JavaScript – they have a Babel preprocessor which makes trying out ES6 a breeze. Another alternative that’s also quite useful is to use Babel’s online REPL – it’ll show you compiled ES5 code to the right of your ES6 code for quick comparison.

Before getting into it, let me shamelessly ask for your support if you’re enjoying my ES6 in Depth series. Your contributions will go towards helping me keep up with the schedule, server bills, keeping me fed, and maintaining Pony Foo as a veritable source of JavaScript goodies.

Thanks for reading that, and let’s go into collections now! For a bit of context, you may want to check out the article on iterators – which are closely related to ES6 collections – and the one on spread and rest parameters.

Now, let’s pick up where we left off – it’s time for WeakMap.

ES6 WeakMaps

You can think of WeakMap as a subset of Map. There are a few limitations on WeakMap that we didn’t find in Map. The biggest limitation is that WeakMap is not iterable, as opposed to Map – that means there is no iterable protocol, no .entries(), no .keys(), no .values(), no .forEach() and no .clear().

Another “limitation” found in WeakMap as opposed to Map is that every key must be an object, and value types are not admitted as keys. Note that Symbol is a value type as well, and they’re not allowed either.

var map = new WeakMap()
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key

This is more of a feature than an issue, though, as it enables map keys to be garbage collected when they’re only being referenced as WeakMap keys. Usually you want this behavior when storing metadata related to something like a DOM node, and now you can keep that metadata in a WeakMap. If you want all of those you could always use a regular Map as we explored earlier.

You are still able to pass an iterable to populate a WeakMap through its constructor.

var map = new WeakMap([[new Date(), 'foo'], [() => 'bar', 'baz']])

Just like with Map, you can use .has, .get, and .delete too.

var date = new Date()
var map = new WeakMap([[date, 'foo'], [() => 'bar', 'baz']])
console.log(map.has(date))
// <- true
console.log(map.get(date))
// <- 'foo'
map.delete(date)
console.log(map.has(date))
// <- false

Is This a Strictly Worse Map?

I know! You must be wondering – why the hell would I use WeakMap when it has so many limitations when compared to Map?

The difference that may make WeakMap worth it is in its name. WeakMap holds references to its keys weakly, meaning that if there are no other references to one of its keys, the object is subject to garbage collection.

Use cases for WeakMap generally revolve around the need to specify metadata or extend an object while still being able to garbage collect it if nobody else cares about it. A perfect example might be the underlying implementation for process.on('unhandledRejection') which uses a WeakMap to keep track of promises that were rejected but no error handlers dealt with the rejection within a tick.

Keeping data about DOM elements that should be released from memory when they’re no longer of interest is another very important use case, and in this regard using WeakMap is probably an even better solution to the DOM-related API caching solution we wrote about earlier using Map.

In so many words then, no. WeakMap is not strictly worse than Map – they just cater to different use cases.

ES6 Sets

Sets are yet another collection type in ES6. Sets are very similar to Map. To wit:

  • Set is also iterable
  • Set constructor also accepts an iterable
  • Set also has a .size property
  • Keys can also be arbitrary values
  • Keys must be unique
  • NaN equals NaN when it comes to Set too
  • All of .keys, .values, .entries, .forEach, .get, .set, .has, .delete, and .clear

However, there’s a few differences as well!

  • Sets only have values
  • No set.get – but why would you want get(value) => value?
  • Having set.set would be weird, so we have set.add instead
  • set[Symbol.iterator] !== set.entries
  • set[Symbol.iterator] === set.values
  • set.keys === set.values
  • set.entries() returns an iterator on a sequence of items like [value, value]

In the example below you can note how it takes an iterable with duplicate values, it can be spread over an Array using the spread operator, and how the duplicate value has been ignored.

var set = new Set([1, 2, 3, 4, 4])
console.log([...set])
// <- [1, 2, 3, 4]

Sets may be a great alternative to work with DOM elements. The following piece of code creates a Set with all the <div> elements on a page and then prints how many it found. Then, we query the DOM again and call set.add again for every DOM element. Since they’re all already in the set, the .size property won’t change, meaning the set remains the same.

function divs () {
  return [...document.querySelectorAll('div')]
}
var set = new Set(divs())
console.log(set.size)
// <- 56
divs().forEach(div => set.add(div))
console.log(set.size)
// <- 56
// <- look at that, no duplicates!

ES6 WeakSets

Much like with WeakMap and Map, WeakSet is Set plus weakness minus the iterability – I just made that term up, didn’t I?

That means you can’t iterate over WeakSet. Its values must be unique object references. If nothing else is referencing a value found in a WeakSet, it’ll be subject to garbage collection.

Much like in WeakMap, you can only .add, .has, and .delete values from a WeakSet. And just like in Set, there’s no .get.

var set = new WeakSet()
set.add({})
set.add(new Date())

As we know, we can’t use primitive values.

var set = new WeakSet()
set.add(Symbol())
// TypeError: invalid value used in weak set

Just like with WeakMap, passing iterators to the constructor is still allowed even though a WeakSet instance is not iterable itself.

var set = new WeakSet([new Date(), {}, () => {}, [1]])

Use cases for WeakSet vary, and here’s one from a thread on es-discuss – the mailing list for the ECMAScript-262 specification of JavaScript.

const foos = new WeakSet()
class Foo {
  constructor() {
    foos.add(this)
  }
  method () {
    if (!foos.has(this)) {
      throw new TypeError('Foo.prototype.method called on incompatible object!')
    }
  }
}

As a general rule of thumb, you can also try and figure out whether a WeakSet will do when you’re considering to use a WeakMap as some use cases may overlap. Particularly, if all you need to check for is whether a reference value is in the WeakSet or not.

Next week we’ll be having Proxy for brunch :)

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments