Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

So 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:

7 lines play.js
let foods = {};
foods.italian = 'gelato';
foods.mexican = 'tortas';
foods.canadian = 'poutine';
... lines 5 - 7

At the bottom, of course, we can log foods.italian:

7 lines play.js
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'):

7 lines play.js
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'):

7 lines play.js
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'):

10 lines play.js
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:

13 lines play.js
... lines 1 - 5
let southernUsStates = ['Tennessee', 'Kentucky', 'Texas'];
... lines 7 - 13

Now we can say foods.set(southernUSStates) and set that to hot chicken:

13 lines play.js
... 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:

13 lines play.js
... 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:

14 lines play.js
... 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:

14 lines play.js
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:

14 lines play.js
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:

15 lines play.js
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!

Leave a comment!

6
Login or Register to join the conversation
Default user avatar
Default user avatar J.R. Jenkins | posted 5 years ago

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?

Reply

Hey Arber,

Thanks for sharing this. Though, you sent a link about .has() method, so contains() is still does not exist. ;)

Cheers!

Reply

Yo J.R. Jenkins!

Woh! This is a mystery... even to me! You're absolutely right - this method does not exist... and unfortunately, I can't even remember what the heck I was thinking or where this came from!? It was not removed, it was just simply not ever there :). We'll edit that out of the video - very good catch! I'm glad you challenged us on that :).

Cheers!

Reply
Default user avatar

Hey Ryan!

No problem, but worry about that edit after you finish the Web Pack Series!! :-) I am on the edge of my seat following along at that one as you have really demystified some aspects of Web Pack that I just couldn't wrap my head around before.

As always thanks for the great tutorials.

-jrj

Reply

Ah, awesome! I have *loved* writing the Webpack tutorial. We'll keep having a new piece out each day :).

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses Symfony 3. But, since this is a JavaScript tutorial, all the concepts work fine in newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "symfony/symfony": "3.2.*", // v3.2.14
        "twig/twig": "2.10.*", // v2.10.0
        "doctrine/orm": "^2.5", // v2.7.1
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.2
        "symfony/swiftmailer-bundle": "^2.3", // v2.4.2
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.3.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.19
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0@dev", // dev-master
        "doctrine/doctrine-fixtures-bundle": "~2.3", // v2.4.1
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.2.1
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "friendsofsymfony/jsrouting-bundle": "^1.6" // 1.6.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.0" // v3.2.2
    }
}
userVoice