A Look at ES6 Maps

| Comments

The Mastering the Arcane Art of JavaScript-mancy series are my humble attempt at bringing my love for JavaScript to all other C# developers that haven’t yet discovered how awesome this language and its whole ecosystem are. These articles are excerpts of the super duper awesome JavaScript-Mancy book a compendium of all things JavaScript for C# developers.

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

JavaScript’s Map

You can experiment with all examples in this article directly within this jsBin

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
2
3
4
5
6
wizardsArchive.set('jaime', {
   name: 'jaime',
   title: 'The Bold',
   race: 'ewok',
   traits: ['joyful', 'hairless']
});

And get to retrieve it:

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

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
2
3
4
5
6
7
wizardsArchive.set(42, "What is the answer to life, the universe and everything?")
console.log(wizardsArchive.get(42));
// => What is the answer to life, the universe and everything?

wizardsArchive.set('firebolt', (target) => console.log(`${target} is consumed by fire`));
wizardsArchive.get('firebolt')('frigate');
// => frigate is consumed by fire

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

1
2
console.log(`there are ${wizardsArchive.size} thingies in the archive`)
// => 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 non-sensical items from the last example:

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

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

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

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

1
2
3
wizardsArchive.clear();
console.log(`there are ${wizardsArchive.size} wizards in the archive`);
// => 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// let's add some items back so we have something to iterate over...
// the set method is chainable by the by!
wizardsArchive
    .set('jaime', {name: 'jaime', title: 'The Bold', race: 'ewok',
       traits: ['joyful', 'hairless']})
    .set('theRock', {name: 'theRock', race: 'giant',
       traits: ['big shoulders']});

for(let keyValue of wizardsArchive){
  console.log(`${keyValue[0]} : ${JSON.stringify(keyValue[1])}`);
}
/*
"jaime : {\"name\":\"jaime\",\"race\":\"....
"theRock : {\"name\":\"theRock\",\"race\....
*/

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

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

And you can improve it greatly if you use the destructuring syntax to extract the key and the value directly:

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

Much nicer right? Alternatively you can use the Map.prototype.forEach method that works in a very similar way to Array.prototype.forEach but with keys and values:

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

In addition to iterating over key-value pairs, Map offers an iterator for keys:

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

And another for values:

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

Both of which provide you with a better developer experience in those cases where you just need the keys or the values.

In these examples above we created an Array from the keys and the values iterator and concatenated its elements using join. That 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

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
2
3
4
5
let jaimeTheWizard = {name: 'jaime', title: 'The Bold', race: 'ewok', traits: ['joyful', 'hairless']};
let theRock = {name: 'theRock', title: 'The Mighty', race: 'giant', trait: ['big shoulders']};
let randalfTheRed = {name: 'randalf', title: 'The Red', race: 'human', traits: ['pyromaniac']};

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
2
3
4
5
6
var wizardsByRace = new Map(wizards.map(w => [/*key*/ w.race, /*value*/ w]));

console.log(Array.from(wizardsByRace.keys()));
// => ["ewok", "giant", "human"]
console.log(wizardsByRace.get("human").name);
// => randalf

The Map constructor expects to find an iterator that iterates over key/value pairs represented as an array where the first element is the key and the second element is the value. 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 a key/value pair, which are the race of the wizard and the wizard itself.

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

1
2
3
4
5
// we can formalize it by creating a helper method to
function* toKeyValue(arr, keySelector){
  for(let item of arr)
    yield [keySelector(item), item];
}

The toKeyValue function above is a generator, a special function that helps you build iterators. Right now, you just need to understand that we are transforming each element of an array into a key value pair. (If you are interested in learning more about iterators and generators check this article)

Indeed, we can use the generator to transform the array into a collection of key value pairs:

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

And then pass in this new collection to the Map constructor:

1
2
3
4
5
var keyValues = toKeyValue(wizards, w => w.name);
var wizardsByName = new Map(keyValues);

console.log(Array.from(wizardsByName.keys()));
// => ["jaime", "theRock", "randalf"]

You could even extend the Array.prototype to provide a nicer API:

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

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

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

And you would bring it one step further by creating a toMap function:

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

Concluding

In this article you learnt how you can take advantage of the new Map data structure to store data by an arbitray key. Map is JavaScript’s implementation of a HashTable or Dictionary in C# where you can use any type as key and as value. You 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.

Buy the Book!

Are you a C# Developer interested in learning JavaScript? Then take a look at the JavaScript-mancy book, a complete compendium of JavaScript for C# developers.

a JavaScriptmancy sample cover

Have a great week ahead!! :)

More Articles in These Series

Comments