A look at ES6 Maps
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:
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
:
wizardsArchive.set('jaime', {
name: 'jaime',
title: 'The Bold',
race: 'ewok',
traits: ['joyful', 'hairless'],
})
And get
to retrieve it:
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!:
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:
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:
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:
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:
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:
// 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:
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:
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:
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
:
console.log(Array.from(wizardsArchive.keys()).join(', '))
// => jaime, theRock"
And another for values
:
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:
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:
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:
// 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:
var keyValues = toKeyValue(wizards, w => w.name)
And then pass in this new collection to the Map
constructor:
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:
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:
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:
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.
Have a great week ahead!! :)
More Articles In This Series
- Chapter 0: The Basic Ingredients of JavaScriptMancy
- Chapter 1: The Many a One JavaScript Quirks
- Functions
- Object Oriented Programming
- Basics
- ES6
- OOP
- Introduction to OOP in JavaScript for C# Developers
- Summoning Fundamentals: Introduction to OOP In JavaScript – Encapsulation
- Summoning Fundamentals: Introduction to OOP In JavaScript – Inheritance
- Summoning Fundamentals: Introduction to OOP In JavaScript – Polymorphism
- White Tower Summoning: Mimicking C# Classical Inheritance in Javascript
- White Tower Summoning Enhanced: The Marvels of ES6 Classes
- Black Tower Summoning: Object Composition with Mixins
- Safer JavaScript Composition With Traits and Traits.js
- JavaScript Ultra Flexible Object Oriented Programming with Stamps
- Object Oriented JavaScript for C# Programmers
- Tooling
- Data Structures
- Functional Programming
- And more stuff to be shaped out:
- Functional Programming with JavaScript
- Modules
- Asynchronous Programming
- The JavaScript Ecosystem
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.