Map and WeakMap
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeSo far, all the new ES2015 stuff has been new language constructs: new syntaxes and keywords, like let
, const
and classes! And that was no accident: these are the most important things to understand.
But ES2015 comes packed with other new features, like new functions and new objects. And mostly, those are easy enough to understand: when you see an object or function you don't recognize, look it up, see how it works... and keep going!
The Map Object
But, there is one set of objects... pun intended... that I do want to talk about. They are, Map
, WeakMap
and... Set
!
Head back into play.js
. Let's experiment with Map
first.
Right now, when you need an associative array, you just create an object: foods = {}
and start adding delicious things to it: foods.italian = 'gelato'
, foods.mexican = 'torta'
and foods.canadian = 'poutine'
. Poutine is super delicious:
let foods = {}; | |
foods.italian = 'gelato'; | |
foods.mexican = 'tortas'; | |
foods.canadian = 'poutine'; | |
// ... lines 5 - 7 |
At the bottom, of course, we can log foods.italian
:
let foods = {}; | |
foods.italian = 'gelato'; | |
foods.mexican = 'tortas'; | |
foods.canadian = 'poutine'; | |
console.log(foods.italian); |
And no surprise, our console tells us we should eat gelato. Good idea!
In ES2015, we now have a new tool: instead of creating a simple object, we can create a new Map
object. The syntax is slightly different: instead of foods.italian = 'gelato'
, use foods.set('italian', 'gelato')
:
let foods = new Map(); | |
foods.set('italian', 'gelato'); | |
// ... lines 3 - 7 |
Repeat this for the other two keys. And at the bottom, fetch the value with foods.get('italian')
:
let foods = new Map(); | |
foods.set('italian', 'gelato'); | |
foods.set('mexican', 'tortas'); | |
foods.set('canadian', 'poutine'); | |
console.log(foods.get('italian')); |
Simple and beautiful! And it works exactly like before!
Great! So... we have a new Map
object... and it's a different way to create an associative array. But why would we use it? Because it comes with some nice helper methods! For example, we can say foods.has('french')
:
let foods = new Map(); | |
foods.set('italian', 'gelato'); | |
foods.set('mexican', 'tortas'); | |
foods.set('canadian', 'poutine'); | |
console.log( | |
foods.get('italian'), | |
foods.has('french') | |
); |
And that returns false
. Bummer for us.
It wasn't too difficult to check if a key existed before, but this feels clean.
Map with Non-String Keys
Map has one other advantage... which is kind of crazy: you can use non-string keys!
Try this: create a new variable: let southernUSStates
set to an array of Tennessee
, Kentucky
, and Texas
:
// ... lines 1 - 5 | |
let southernUsStates = ['Tennessee', 'Kentucky', 'Texas']; | |
// ... lines 7 - 13 |
Now we can say foods.set(southernUSStates)
and set that to hot chicken
:
// ... lines 1 - 5 | |
let southernUsStates = ['Tennessee', 'Kentucky', 'Texas']; | |
foods.set(southernUsStates, 'hot chicken'); | |
// ... lines 8 - 13 |
Yes, the key is actually an object. And that's no problem!
Important side note: hot chicken is really only something you should eat in Tennessee, but for this example, I needed to include a few other states. In Texas, you should eat Brisket.
Anyways, at the bottom, use foods.get(southernUSStates)
to fetch out that value:
// ... lines 1 - 5 | |
let southernUsStates = ['Tennessee', 'Kentucky', 'Texas']; | |
foods.set(southernUsStates, 'hot chicken'); | |
console.log( | |
foods.get('italian'), | |
foods.get(southernUsStates) | |
); |
And it works just like we want!
If you're wondering when this would be useful... stay tuned. Oh, and there's one other property you should definitely know about: foods.size
:
// ... lines 1 - 5 | |
let southernUsStates = ['Tennessee', 'Kentucky', 'Texas']; | |
foods.set(southernUsStates, 'hot chicken'); | |
console.log( | |
foods.get('italian'), | |
foods.get(southernUsStates), | |
foods.size | |
); |
That will print 4. Say hello to the new Map
object!
Tip
You can also loop over a Map
using our new friend - the for of
loop. You can
loop over the values or the keys!
// loop over the keys *and* values
for (let [countryKey, food] of foods.entries()) {
console.log(countryKey, food); // e.g. italian gelato
}
// loop over the keys (e.g. italian)
for (let countryKey of foods.keys()) {
console.log(countryKey);
}
Behind the scenes, the last example uses destructuring to assign
each returned by entries()
to the countryKey
and food
variables. It's all
coming together!
Introducing WeakMap... a worse Map?
ES2015 also gives us a very similar new object: WeakMap
:
let foods = new WeakMap(); | |
// ... lines 2 - 14 |
And this is where things get a little nuts. Why do we have a Map
and a WeakMap
?
Let's find out! First try to run our code with WeakMap
.
Woh, it explodes!
Invalid value used as week map key
Map
and WeakMap
are basically the same... except WeakMap
has an extra requirement: its keys must be objects. So yes, for now, it seems like WeakMap
is just a worse version of Map
.
Turn each key into an array, which is an object. At the bottom, use foods.get()
and pass it the italian
array:
let foods = new WeakMap(); | |
foods.set(['italian'], 'gelato'); | |
foods.set(['mexican'], 'tortas'); | |
foods.set(['canadian'], 'poutine'); | |
// ... lines 5 - 8 | |
console.log( | |
foods.get(['italian']), | |
// ... lines 11 - 12 | |
); |
Now when I run it, it works fine. Wait, or, does it?
Two interesting things: this prints undefined
, hot chicken
, undefined
. First, even though the ['italian']
array in get()
is equal to the ['italian']
array used in set, they are not the same object in memory. These are two distinct objects, so it looks like a different key to WeakMap
. That's why it prints undefined
.
Second, with WeakMap
, you can't call foods.size
. That's just not something that works with WeakMap
.
WeakMap and Garbage Collection
Let me show you one other crazy thing, which will start to show you the purpose of WeakMap
. After we set the southernUSStates
onto the array, I'm going to set southernUSStates
to null:
let foods = new WeakMap(); | |
foods.set(['italian'], 'gelato'); | |
foods.set(['mexican'], 'tortas'); | |
foods.set(['canadian'], 'poutine'); | |
let southernUsStates = ['Tennessee', 'Kentucky', 'Texas']; | |
foods.set(southernUsStates, 'hot chicken'); | |
southernUsStates = null; | |
// ... lines 9 - 15 |
When you try it now, this of course prints "undefined". That makes sense: we're now passing null
to the get()
function.
But what you can't see is that the southernUSStates
object no longer exists... anywhere in memory!
Why? In JavaScript, if you have a variable that isn't referenced by anything else anymore, like southernUSStates
, it's eligible to be removed by JavaScript's garbage collection. The same thing happens in PHP.
But normally, because we set southernUSStates
as a key on WeakMap
, this reference to southernUSStates
would prevent that garbage collection. That's true with Map
, but not WeakMap
: it does not prevent garbage collection. In other words, even though southernUSStates
is still on our WeakMap
, since it's not being referenced anywhere else, it gets removed from memory thanks to garbage collection.
But, really, how often do you need to worry about garbage collection when building a web app? Probably not very often. So, at this point, you should just use Map
everywhere: it's easier and has more features.
And that's true! Except for one special, fascinating, nerdy WeakMap
use-case. Let's learn about it!
You mention that the Map object has a contains method, but I don't see that listed at https://developer.mozilla.o... and testing it in play.js doesn't seem to work. Was this removed or am I missing something?