Organizing Your Data With ES6 Maps

It is unknown who designed the first map.

But it is said that he got tired of traversing
a whole array of shoes every time he decided
to go for a walk.

  
        - Ckor Srich
        Royal Buffoon 2nd Age

Take a Look at These Maps

ES6 brings two new data structures to JavaScript, the Map and the Set. This chapter is devoted to the Map, which is fundamentally a HashTable. We often refer to it as Dictionary in C#. JavaScript’s Map provides a simple API to store objects by an arbitrary key, a very essential functionality required in many JavaScript programs.

JavaScript’s Map

You can create a Map in JavaScript using the new operator:

1 const wizardsArchive = new Map();

Once created the Map offers two fundamental methods: get and set. As you can probably guess using your wizardy intuition, you use set to add an object to the Map:

1 wizardsArchive.set( /* key */ 'jaime', /* value */ {
2    name: 'jaime', 
3    title: 'The Bold', 
4    race: 'ewok', 
5    traits: ['joyful', 'hairless']
6 });

And get to retrieve it:

1 console.log('Wizard with key jaime => ', wizardsArchive.get('jaime')\
2 );
3 /* => Item with key jaime =>
4   [object Object] {
5     name: "jaime",
6     race: "ewok",
7     trait: ["joyful", "hairless"]
8   }
9 */

This being JavaScript you can use any type as key or value, and the same Map can hold disparate types for both keys and values. Yey! Freedom!:

1 wizardsArchive.set(42, "What is the answer to life, the universe and\
2  everything?")
3 console.log(wizardsArchive.get(42));
4 // => What is the answer to life, the universe and everything?
5 
6 wizardsArchive.set('firebolt', (target) => console.log(`${target} is\
7  consumed by fire`));
8 wizardsArchive.get('firebolt')('frigate');
9 // => frigate is consumed by fire

You can easily find how many elements are stored within a Map using the size property:

1 console.log(`there are ${wizardsArchive.size} thingies in the archiv\
2 e`)
3 // => there are 3 thingies in the archive

Removing items from a Map is very straightforward as well, you use the delete method with the item’s key. Let’s do some cleanup and remove those nonsensical items from the last example:

1 wizardsArchive.delete(42);
2 wizardsArchive.delete('firebolt');

Now that we have removed them, we can verify that indeed they are gone using the has method:

1 console.log(`Wizards archive has info on '42': ` +
2             `${wizardsArchive.has(42)}`);
3 // => Wizards archive has info on '42': false
4 console.log(`Wizards archive has info on 'firebolt': 
5   ${wizardsArchive.has('firebolt')}`);
6 // => Wizards archive has info on 'firebolt': false

And when we are done for the day and want to remove every item at once, the Map offers the clear method:

1 wizardsArchive.clear();
2 console.log(`there are ${wizardsArchive.size} wizards in the archive\
3 `);
4 // => there are 0 wizards in the archive

Iterating Over the Elements of a Map

Just like with arrays you can iterate over the elements of a Map using the for/of loop:

 1 // let's add some items back so we have something to iterate over...
 2 // the set method is chainable by the by!
 3 wizardsArchive
 4     .set('jaime', {name: 'jaime', title: 'The Bold', race: 'ewok', 
 5        traits: ['joyful', 'hairless']})
 6     .set('theRock', {name: 'theRock', race: 'giant', 
 7        traits: ['big shoulders']});
 8 
 9 for(let keyValue of wizardsArchive){
10   console.log(`${keyValue[0]} : ${JSON.stringify(keyValue[1])}`);
11 }
12 /*
13 "jaime : {\"name\":\"jaime\",\"race\":\"....
14 "theRock : {\"name\":\"theRock\",\"race\....
15 */

The default Map iterator (also available via the entries property) lets you traverse a Map using key-value pairs. Each pair is an array with two items, the first being the key and the second the value. The example above is equivalent to:

1 for(let keyValue of wizardsArchive.entries()){
2   console.log(`${keyValue[0]} : ${JSON.stringify(keyValue[1])}`);
3 }

Both examples above are a little displeasing to the eye, aren’t they? You can improve them greatly if you use the destructuring syntax to extract the key and the value from the key-value array:

1 for(let [key, value] of wizardsArchive){
2   console.log(`${key} : ${JSON.stringify(value)}`);
3 }

Much nicer right? Alternatively you can use the Map.prototype.forEach method analogous to Array.prototype.forEach but with keys and values:

1 wizardsArchive.forEach((key,value) => 
2   console.log(`${key} : ${JSON.stringify(value)}`)
3 );
4 // => jaime: {\"name\" ...
5 // => theRock: {\"name\" ...

In addition to iterating over key-value pairs, you can also traverse the keys:

1 console.log(Array.from(wizardsArchive.keys()).join(', '));
2 // => jaime, theRock"

And the values:

1 console.log(Array.from(wizardsArchive.values())
2                  .map(i => i.race).join(', '));
3 // => ewok, giant

Both the keys and values iterators provide a better developer experience in those cases where you just need the keys or the values.

Note that in the examples above we created an Array from the keys and the values iterators and concatenated its elements using join. This resulted in us “iterating” over the whole Map at once, but we could have just as well used a for/of loop and operated on each item separately.

Creating a Map From an Iterable Collection

In addition to creating empty Maps and filling them with information, you can create Maps from any iterable collection. For instance, let’s say that you have an array of wizards:

1 let jaimeTheWizard = {name: 'jaime', title: 'The Bold', 
2                   race: 'ewok', traits: ['joyful', 'hairless']};
3 let theRock = {name: 'theRock', title: 'The Mighty', 
4                race: 'giant', trait: ['big shoulders']};
5 let randalfTheRed = {name: 'randalf', title: 'The Red', 
6                      race: 'human', traits: ['pyromaniac']};
7 
8 let wizards = [jaimeTheWizard, theRock, randalfTheRed];

And you want to group them by race and put them on a dictionary where you can easily find them. You can do that by passing a suitably shaped collection into the Map constructor:

1 var wizardsByRace = new Map(wizards
2                         .map(w => [/*key*/ w.race, /*value*/ w]));
3 
4 console.log(Array.from(wizardsByRace.keys()));
5 // => ["ewok", "giant", "human"]
6 console.log(wizardsByRace.get("human").name);
7 // => randalf

The Map constructor expects to find an iterator that goes through key-value pairs represented as an array where the first element is the key and the second element is the value:

1 [[key1, value1], [key2, value2], ...]

In the example above we used map over the wizards array to transform each element of the original array into a new one that represents key-value pairs, which are the race of the wizard and the wizard itself.

1 [["ewok", jaimeTheWizard], ["giant", theRock], 
2  ["human", randalfTheRed]]

We could create a helper method toKeyValue to make this transformation easier:

1 function* toKeyValue(arr, keySelector){
2   for(let item of arr) 
3     yield [keySelector(item), item];
4 }

The toKeyValue function above is a generator, a special function that helps you build iterators. You’ll learn more about generators later in this tome on data structures. For the time being, you just need to understand that we are transforming each element of an array into a key value pair.

When we call the generator we effectively transform the array into an iterator of key value pairs:

1 var keyValues = toKeyValue(wizards, w => w.name)

We can pass this new iterator to the to the Map constructor and obtain the desired Map of wizards:

1 var keyValues = toKeyValue(wizards, w => w.name);
2 var wizardsByName = new Map(keyValues);
3 
4 console.log(Array.from(wizardsByName.keys()));
5 // => ["jaime", "theRock", "randalf"]

We still need to perform the transformation in two separate steps which is not very developer friendly. We can improve this by extending the Array.prototype with a toKeyValue method:

1 Array.prototype.toKeyValue = function* toKeyValue(keySelector){
2   for(let item of this)
3     yield [keySelector(item), item];
4 }

This would allow you to rewrite the previous example like this:

1 var wizardsByTitle = new Map(wizards.toKeyValue(w => w.title));
2 console.log(Array.from(wizardsByTitle.keys()));
3 // => ["The Bold", "The Mighty", "The Red"]

You could even bring it one step further by creating a toMap function:

1 Array.prototype.toMap = function(keySelector) {
2   return new Map(this.toKeyValue(keySelector));
3 }
4 var wizardsByTitle = wizards.toMap(w => w.title);
5 console.log(Array.from(wizardsByTitle.keys()));
6 // => ["The Bold", "The Mighty", "The Red"]

Map Cheatsheet

Basic Operations

Basics description
var map = new Map() Create an empty map
var map = new Map(iterator) Create a map from an iterator
var value = map.get(‘key’) Get a value from the map by key. If the key is not in the map it returns undefined. The key can be of any type.
map.set(‘key’, ‘value’) Add an item to the map. If the key already exists within the map the value is overwritten. The key and value can be of any type. Chainable.
map.delete(‘key’) Remove item by key if it exists. Returns true if an item has been removed and false otherwise.
map.has(‘key’) Check whether a key exists in the map. Returns true or false whether the key exists or not respectively.
map.size Returns the number of items in the map
map.clear() Remove all items within the map

Iterating a Map

Methods of Iteration description
map.forEach((key,value,map) ⇒ {}) Iterate over every key value pair within a map
map.entries() Returns a key/value pair iterator. This is the default iterator in Map.prototype[Symbol.iterator]
map.keys() Returns a key iterator
map.values() Returns a values iterator

Concluding

In this chapter you learnt how you can take advantage of the new Map data structure to store data by an arbitrary key of your choice. Map is JavaScript’s implementation of a HashTable, or a Dictionary in C#, where you can use any type as key and as value.

You also learnt about the basic operations you can perform with a Map, how you can store, retrieve and remove data, check whether or not a key exists within the Map and how to iterate it in different ways.

Exercises