2015 was a great year for JavaScript - the language received a much-awaited significant update, by the name of ECMAScript 6 (a.k.a. ES6, a.k.a. ECMAScript 2015 ¯_(ツ)_/¯), the first update to the language since ES5 was standardized back in 2009. Among many features four newly formed data structures were introduced: Map
, Set
, WeakMap
, WeakSet
.
Surprisingly to me, six years have already passed since ES6 initial release and after all that time some of these data structures still feel so new and fresh. With all of that being said, feeling the pressure of my ever-growing impostor syndrome, I've decided to refresh my memory on one of these lovely structures - Map
. And if you're in the same boat as me (don't worry, there's nothing wrong with you) let's explore together what this thing can do.
Same same, but different, but still the same
If you’ve been on the internet long enough you’ve probably encountered the meme before and it kind of relates to Map
in a way. Map
is quite similar to the well-known Object
that you’ve been using for ages. So what is Map
after all?
It’s a data structure that holds key-value pairs, just like our friend Object
. Of course it has it’s fair share of differences, but the similarity is so apparent that historically Object
has been used as Map
(there were no other alternatives). Just look how readable and understandable this code snippet is when you have that mental model in your head:
const pokemons = new Map()
pokemons.set('pikachu', { category: 'Mouse', type: 'Electric' })
pokemons.set('bulbasaur', { category: 'Seed', type: 'Grass' })
pokemons.get('pikachu') // { category: 'Mouse', type: 'Electric' }
pokemons.get('meowth') // undefined
pokemons.size // 2
pokemons.has('pikachu') // true
pokemons.delete('pikachu') // true
pokemons.has('pikachu') // false
pokemons.clear()
pokemons.size // 0
Sure, the API is different, but I’m pretty sure that you understand what this code does and what its purpose is by just looking at it. Essentially what we’re doing here is creating a new Map
instance, setting some values, deleting them, checking the size, your standard stuff.
Instead of setting values as properties as we would on an Object
(which you can also do on Map
, but please don’t do that) we use this nifty API that Map
gives us. This opens up some new capabilities like checking the size of an instance, like we did on line 9 with pokemons.size
, which we can not do on an Object
instance.
You could also initialize a Map
with pre-existing values if you wanted to:
const pokemons = new Map([
['pikachu', { category: 'Mouse', type: 'Electric' }],
['bulbasaur', { category: 'Seed', type: 'Grass' }]
])
I’m not going to bore you by describing every method that exists on Map
, but if you’re interested here’s a good place to start: Map, Instance methods — JavaScript | MDN.
But different…?
Now that we know what Map
is and how it functions let’s explore the more interesting and impactful differences that it has compared to an Object
.
Key types and accidental keys
Although it make come as a surprise keys of an Object
are always either a String
or a Symbol
. What does that mean for us? Well, for example, that means that the Object
keys can not be a Number
. In the following code snippet obj[1]
key will be coerced to a String
.
const obj = {}
obj[1] = 'probablyOne'
obj['1'] // 'probablyOne'
That’s not the only limitation when it comes to keys in an Object
, you might accidentally override a default Object
key, like toString
method for example. To be honest I can’t recall a situation where I would run into this particular “problem”, but I guess technically it could be an issue.
These problems don’t exist on a Map
. It does not give a single flying duck what its key is. Want to give it a Number
as a key? Yep.
Maybe a Boolean
, Function
or even an Object
? No problem what so ever.
This type of functionality is quite useful when you’re not sure what type of keys you will be using. If the key is specified from an external source (say a user input or an API call response) Map
is a good candidate to solve that problem. Or if you just want to use Number
, Function
or whatever type as a key instead of String
, Map
got you covered.
const pagesSectionsMap = new Map()
pagesSectionsMap.set(1, 'Introduction')
pagesSectionsMap.set(50, 'Entering the shadow realm')
pagesSectionsMap.get(1) // 'Introduction'
pagesSectionsMap.get(50) // 'Entering the shadow realm'
Order and iteration
Object
is a nonordered data structure, meaning that it does not care about the sequence in which your key-value pairs were entered. Well, it actually does have an “order” now, but it’s hard to understand, there’s tons of rules and it’s just better not to rely on it, since the possibility of introducing a bug is relatively high.
It also does not implement an iteration protocol, meaning that objects are not iterable using for...of
statement. You can get an iterable object using Object.keys
or Object.entries
though.
On the other hand Map
is ordered, it remembers the original sequence of your key-value pairs and it also plays nicely with the iteration protocol. Cool. Let’s take a look how that might be useful.
const userFavPokemonMap = new Map()
userFavPokemonMap.set('John', { name: 'Pikachu', type: 'Electric' })
userFavPokemonMap.set('Jane', { name: 'Bulbasaur', type: 'Grass' })
userFavPokemonMap.set('Tom', { name: 'Meowth', type: 'Normal' })
for ([user, favouritePokemon] of userFavPokemonMap) {
console.log(user) // 'John', 'Jane', 'Tom'
}
Now you might be thinking: “Who cares what order these will be printed out?”. Little did you know that John and Jane are low-key maniacs and they like to be first everywhere. In all seriousness though maybe this is not the best example, but hopefully it conveys the concept. If anyone sees an obvious use case where order is important and it’s related to pokemons, let me know.
You could even use other methods that exist on Map
and iterate through them in the same fashion:
for (name of userFavPokemonMap.keys()) {
console.log(name)// "John", "Jane", "Tom"
}
for (pokemon of userFavPokemonMap.values()) {
console.log(pokemon) // { name: "Pikachu", type: "Electric" }, ..
}
You could even forEach
this bad boy if you wanted to:
userFavPokemonMap.forEach((favPokemon, name) => {
console.log(name)
})
I want to reiterate that we could achieve almost the same functionality using a plain old Object
, but if we care about the order of our values Map
is definitely the way to go.
Performance
Map
has some distinct performance improvements when it comes to frequent additions and removals of key-value pairs unlike Object
. If you ever find yourself in a position where you need to get some performance gains on that front Map
just might be your new friend that comes to save the day.
Serialization and parsing
This might be a bummer for some of you, because Map
does not offer any serialization or parsing capabilities. That means that if we use JSON.stringify
or JSON.parse
we’ll not get much.
userFavPokemonMap.set('John', { name: 'Pikachu', type: 'Electric' })
JSON.stringify() // "{}"
You could create your own serialization and parsing if you wanted to of course, here’s how you can do it.
Key equality
Map
uses a SameValueZero algorithm. OK, but what does that mean? Let’s start by looking which equality algorithms currently exist in JavaScript:
- Abstract Equality Comparison (
==
) - Strict Equality Comparison (
===
) - SameValueZero (the one that
Map
uses) - SameValue (
Object.is
)
I’m fairly sure that you’ve definitely encountered ==
or ===
in the wild. Object.is
is something that I personally haven’t seen that often, it’s a bit out of topic, so in case you’re interested you can read more here if you want.
What we’re curious about is SameValueZero and why it’s used in Map
operations. To gain some instant familiarity just imagine that it’s the same as ===
only with some additional quirks.
Quirk no. 1: it treats signed zeroes as the same value. That means that +0
and -0
is the same in Map
eyes.
const numbersMap = new Map()
numbersMap.set(+0, 'nice tutorial')
numbersMap.get(0) // 'nice tutorial'
The only explanation that I could find why this is important is because -0
could easily sneak into your code via an arithmetic operation, but you almost always want -0
to be treated as 0
.
Quirk no. 2: it treats NaN
as equal to other NaN
values.
NaN === NaN // false
const nonNumbersMap = new Map()
nonNumbersMap.set(NaN, 'number?')
nonNumbersMap.get(NaN) // 'number?'
This one is kind of straightforward, as we don’t want to have distinct NaN
values.
That’s all folks. If you’ve made it until the end I just wanna say thanks, that really warms my heart ❤️
Until next time!
Top comments (0)