ECMAScript 6 introduces a lot of cool features that we are now quite familiar with. In this article, we talk about some of the data structures that it implements. To understand more sophisticated ones like the WeakMap, we also go through the regular Map. The above aims to introduce us to some new possibilities and therefore broaden our programming vocabulary.
Map
The Map is in its roots quite similar to a regular object. It provides us with a way to set values associated with a particular key and retrieve or delete them.
1 2 3 |
const pets = new Map(); pets.set('dog', 'Fluffy'); pets.get('dog'); // Fluffy |
What can seem similar at first glance, has some significant differences. While the key of an object can be just a string or a symbol, the key of a Map can be of any type.
1 2 3 4 5 6 |
const dog = { name: 'Fluffy' }; const cat = { name: 'Garfield' }; |
1 2 3 4 5 6 |
const object = { [dog]: 'An object with a dog' } const map = new Map(); map.set(dog, 'An object with a dog'); |
1 2 |
console.log(object[cat]); // An object with a dog console.log(map.get(cat)) // undefined |
The weird thing happens when we try to access the object[cat] even though we didn’t set it up. It turns out that when we try to use an object as a key, it is stringified to [object Object]. Because of the above, both object[cat] and object[dog] yield the same result.
If you want to read more about the type conversions, check out [1] + [2] – [3] === 9!? Looking into assembly code of coercion
A compelling case is with the order of the properties. Some sources claim that an advantage that Maps have over the objects is that they preserve the order of keys. The above is not entirely accurate because the ECMAScript 2015 defined the desired order of the properties. In the description of the OrdinaryOwnPropertyKeys, we see the following order:
- The integer index keys in the ascending order.
- String keys (non-integer), in the order of creation – from the oldest to the newest.
- The symbol keys, in the order of creation
An integer index is a String-valued property key that is a canonical numeric String and whose numeric value is either +0 or a positive integer ≤ 253-1.
When to use maps
Aside from the above differences, are also some useful properties, like Map.prototype.size. You can also iterate your maps effortlessly with the use of the forEach method and the for...of loop.
Another important thing when deciding between the map and a regular object might be the performance caused by differences in the implementation. There is no simple answer to which one is better – it all depends on the use case. According to MDN, it performs better in a situation when you add and remove key-value pairs frequently. Still, objects are perfectly fine when we just need a straightforward structure to store data using strings, symbols, or integers as keys. A particular disadvantage of maps is that they are not serialized easily to JSON. It is also a reason that they are not particularly advised to use with Redux.
WeakMap
The WeakMap, at the first look, is quite similar to a regular Map. The first difference that we encounter is that the keys can’t be primitive values. That means that when it comes to the keys, we are restricted to use objects. Its implementation and purpose is the cause of the above. It is implemented to give us a way to optimize the work of a garbage collector.
If you want to know the ins and outs of the garbage collector in JavaScript, check out Understanding memory management and the garbage collector
Why we might find it useful
When we use a Map using an object as a key, we create a brand new way to access it.
1 2 3 4 5 6 7 8 |
const map = new Map(); const dog = { name: 'Fluffy' }; map.set(dog, 'An object with a dog'); console.log(map.keys().next().value.name); // Fluffy |
It means that the garbage collector can’t remove the object that we’ve used as a key until the map exists. Check out the next example for more practical use:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
window.addEventListener('load', () => { const targets = new Map(); const firstButton = document.querySelector('#button-one'); firstButton.addEventListener('click', (event) => { console.log(`When the button was clicked previously: `, targets.get(event.target)); targets.set(event.target, Date.now()); }); const secondButton = document.querySelector('#button-two'); secondButton.addEventListener('click', (event) => { console.log(`When the button was clicked previously: `, targets.get(event.target)); targets.set(event.target, Date.now()); }); }); |
In the code above, we save the time of the last time we’ve pressed a particular button with the event target being a key. We don’t need to iterate through the targets map and get the list of buttons, so the above creates a leak: the memory that is no longer needed is not released. We still need to keep the event.target object even when we longer need it, for example, when we remove the button from the DOM tree. Due to that, the garbage collector can’t free up the memory even though our map is the only place that references the object.
The solution to the above problem is the WeakMap. Its keys are weakly referenced, which means that they don’t prevent the garbage collector from erasing the keys from memory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
window.addEventListener('load', () => { const targets = new WeakMap(); const firstButton = document.querySelector('#button-one'); firstButton.addEventListener('click', (event) => { console.log(`When the button was clicked previously: `, targets.get(event.target)); targets.set(event.target, Date.now()); }); const secondButton = document.querySelector('#button-two'); secondButton.addEventListener('click', (event) => { console.log(`When the button was clicked previously: `, targets.get(event.target)); targets.set(event.target, Date.now()); }); }); |
Now, our targets variable holds a WeakMap. It can come in handy in situations like that because if we don’t need any of its keys, the garbage collector can remove them from memory. WeakMaps are not enumerable, and therefore we can’t get a list of all the keys, because that would mean that the garbage collector needs to keep them in the memory.
Summary
In this article, we’ve covered two structures that the ES6 introduces: Maps and WeakMaps. Knowing Maps gives us some new possibilities and to avoid hurting our performance, we might sometimes decide to use WeakMaps. Thanks to them, we can provide hints to the garbage collector so that it can free the memory that we no longer need.