CHAPTER 12
Maps and Sets are a welcome to JavaScript. Sets and Maps provide an efficient data structure for common algorithms.
In JavaScript, it has always been a pain mapping objects or primitive values from/to other objects or primitive values. In ES5, the best you could do was map from strings to arbitrary values. The Map data structure in ES6 lets you use arbitrary values as keys.
Let’s look at a quick example of this:
Code Listing: 159
let map = new Map();
map.set('foo', 123); console.log(map.get('foo')); console.log(map.has('foo')); console.log(map.delete('foo')); console.log(map.has('foo')); |
Here is the output from the preceding code:
Code Listing: 160
123 true true false |
Let’s look at another example:
Code Listing: 161
let map = new Map(); map.set('foo', true); map.set('bar', false); console.log(map.size); map.clear(); console.log(map.size); |
Here is the output from the preceding code:
Code Listing: 162
2 0 |
You can set up a map via an iterable over key-value pair. One way is to use an array as follows:
Code Listing: 163
let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'] ]); |
Another way is to chain the set method like so:
Code Listing: 164
let map = new Map() .set(1, 'one') .set(2, 'two') .set(3, 'three'); |
We have several options when iterating over a map. First, consider the following map definition:
Code Listing: 165
let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'] ]); |
Now, let’s take a look at how we can iterate over this map. The first example will iterate over the keys:
Code Listing: 166
for (let key of map.keys()) { console.log(key); } |
We can also iterate over the values:
Code Listing: 167
for (let value of map.values()) { console.log(value); } |
We can iterate over the entries:
Code Listing: 168
for (let entry of map.entries()) { console.log(entry[0], entry[1]); } |
We can obtain the key/value pair of the entries:
Code Listing: 169
for (let [key, value] of map.entries()) { console.log(key, value); } |
Finally, because entries is an iterator itself, we can rewrite the previous in a more terse manner:
Code Listing: 170
for (let [key, value] of map.entries()) { console.log(key, value); } |
The spread operator (…) turns an iterable into the arguments of a function or parameter call. We have several options when iterating over a map. Thus, we can easily spread our map like the following:
Code Listing: 171
let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'] ]); let arr = [...map.keys()]; console.log(arr); |
The preceding code generates the following output:
Code Listing: 172
[ 1, 2, 3 ] |
A WeakMap is a map that doesn’t prevent its keys from being garbage-collected. That means that you can associate data with objects without worrying about memory leaks.
A WeakMap is a data structure whose keys must be objects and whose values can be arbitrary values. It has the same API as Map, with one significant difference: you can’t iterate over the contents. You can’t iterate over the keys, nor the values, nor the entries. You also can’t clear a WeakMap.
Let’s look at the following example:
Code Listing: 173
let _counter = new WeakMap(); let _action = new WeakMap(); class Countdown { constructor(counter, action) { _counter.set(this, counter); _action.set(this, action); } dec() { let counter = _counter.get(this); if (counter < 1) return; counter--; _counter.set(this, counter); if (counter === 0) { _action.get(this)(); } } } let c = new Countdown(2, () => console.log('DONE')); c.dec(); c.dec(); |
The preceding code uses WeakMaps _counter and _action to store private data. This is very elegant in that these WeakMaps are “holding” a reference to the Countdown class, yet they are doing so in a weak manner and, thus, are memory leak safe.
The preceding code generates the following output:
Code Listing: 174
DONE |
Set works with arbitrary values, is fast, and even handles NaN correctly.
Let’s take a look at an example:
Code Listing: 175
let set = new Set(); set.add('red'); console.log(set.has('red')); set.delete('red'); console.log(set.has('red')); |
Here is the output from the preceding code:
Code Listing: 176
true false |
Let’s look at another example:
Code Listing: 177
let set = new Set(); set.add('red'); set.add('green'); console.log(set.size); set.clear(); console.log(set.size); |
Here is the output from the preceding code:
Code Listing: 178
2 0 |
You can set up a set via an iterable over the elements that make up the set. One way is to use an array as follows:
Code Listing: 179
let set = new Set(['red', 'green', 'blue']); |
Another way is to chain the set method like so:
Code Listing: 180
let set = new Set() .add('red') .add('green') .add('blue'); |
Like maps, elements are compared similarly to ===, with the exception of NaN being like any other value. Consider the following:
Code Listing: 181
let set = new Set([NaN]); console.log(set.size); console.log(set.has(NaN)); |
Executing the preceding code produces the following output:
Code Listing: 182
1 true |
Sets are iterable, and the for-of loop works exactly as you would expect. Consider the following example:
Code Listing: 183
let set = new Set(['red', 'green', 'blue']); for (let x of set) { console.log(x); } |
Executing the preceding code produces the following output:
Code Listing: 184
red green blue |
Sets preserve iteration order. That is, elements are always iterated over in the order in which they were inserted.
Now, let’s look at an example using the spread operator (…). As you know it works with iterables and thus allows you to convert a set into an array:
Code Listing: 185
let set = new Set(['red', 'green', 'blue']); let arr = [...set]; console.log(arr); |
Executing the preceding code produces the following output:
Code Listing: 186
[ 'red', 'green', 'blue' ] |
What about going from array to set? Take a look at the following example:
Code Listing: 187
let arr = [3, 5, 2, 2, 5, 5]; let unique = [...new Set(arr)]; for (let x of unique) { console.log(x); } |
The preceding code produces the following output:
Code Listing: 188
3 5 2 |
As you may have noticed, a set does not have duplicate values in it.
Compared to arrays, sets don’t have the methods map() and filter(). However, you can do a simple trick of converting them to arrays and then back again. Consider the following example:
Code Listing: 189
let set = new Set([1, 2, 3]); set = new Set([...set].map(x => x * 2)); // Resulting set: {2, 4, 6} |
Here is an example using filter:
Code Listing: 190
let set = new Set([1, 2, 3, 4, 5]); set = new Set([...set].filter(x => (x % 2) == 0)); // Resulting set: {2, 4} |
A WeakSet is a set that doesn’t prevent its elements from being garbage-collected. A WeakSet doesn’t allow for iteration, looping, or clearing.
Given that you can’t iterate over their elements, there are not very many use cases for WeakSets. They enable you to mark objects and to associate them with Boolean values.
WeakSets only have three methods, and all of them work the same as the set methods: