Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Private Variables & 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

To see a real-world WeakMap use-case, go back into RepLogApp and scroll to the top. Remember, this file holds two classes: RepLogApp and, at the bottom, Helper:

... lines 1 - 2
(function(window, $, Routing, swal) {
class RepLogApp {
... lines 6 - 178
}
/**
* A "private" object
*/
class Helper {
... lines 185 - 212
}
... lines 214 - 231
})(window, jQuery, Routing, swal);

The purpose of Helper is to be a private object that we can only reference from inside of this self-executing function.

Making Helper a Private Object

But check out the constructor for RepLogApp:

... lines 1 - 2
(function(window, $, Routing, swal) {
class RepLogApp {
constructor($wrapper) {
... line 7
this.helper = new Helper(this.$wrapper);
... lines 9 - 26
}
... lines 28 - 178
}
... lines 180 - 231
})(window, jQuery, Routing, swal);

We set Helper onto a helper property. We do this so that we can use it later, inside of updateTotalWeightLifted(). Here's the problem: the helper property is not private. I mean, inside of our template, if we wanted, we could say: repLogApp.helper.calculateTotalWeight().

Dang! We went to all of that trouble to create a private Helper object... and it's not actually private! Lame!

How can we fix this? Here's an idea: above the class, create a new HelperInstance variable set to null:

... lines 1 - 2
(function(window, $, Routing, swal) {
let HelperInstance = null;
class RepLogApp {
... lines 8 - 180
}
... lines 182 - 233
})(window, jQuery, Routing, swal);

Then, instead of setting the new Helper onto a property - which is accessible from outside, say: HelperInstance = new Helper():

... lines 1 - 2
(function(window, $, Routing, swal) {
let HelperInstance = null;
class RepLogApp {
constructor($wrapper) {
this.$wrapper = $wrapper;
HelperInstance = new Helper(this.$wrapper);
... lines 11 - 28
}
... lines 30 - 180
}
... lines 182 - 233
})(window, jQuery, Routing, swal);

And that's it! The HelperInstance variable is not available outside our self-executing function. And of course down below, in updateTotalWeightLifted(), the code will now read: HelperInstance.getTotalWeightString():

... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 6
class RepLogApp {
... lines 8 - 49
updateTotalWeightLifted() {
this.$wrapper.find('.js-total-weight').html(
HelperInstance.getTotalWeightString()
);
}
... lines 55 - 180
}
... lines 182 - 233
})(window, jQuery, Routing, swal);

And just like that, we've made Helper truly private.

Multiple Instances with Map!

Well... you might already see the problem! Even though we're not doing it here, it is legal to create multiple RepLogApp objects. And if we did create two RepLogApp objects, well the second would replace the HelperInstance from the first! We can only ever have one HelperInstance... even though we may have multiple RepLogApp objects. Bad design Ryan!

Ok, so why not use our cool new Map object to store a collection of Helper objects? let HelperInstances = new Map():

... lines 1 - 2
(function(window, $, Routing, swal) {
let HelperInstances = new Map();
... lines 6 - 233
})(window, jQuery, Routing, swal);

In the constructor(), set the new object into that map: HelperInstances.set()... and for the key - this may look a little weird - use this:

... lines 1 - 2
(function(window, $, Routing, swal) {
let HelperInstances = new Map();
class RepLogApp {
constructor($wrapper) {
this.$wrapper = $wrapper;
HelperInstances.set(this, new Helper(this.$wrapper));
... lines 11 - 28
}
... lines 30 - 180
}
... lines 182 - 233
})(window, jQuery, Routing, swal);

In other words, we key this HelperInstance to ourselves, our instance. That means that later, to use it, say HelperInstances.get(this).getTotalWeightString():

... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 6
class RepLogApp {
... lines 8 - 49
updateTotalWeightLifted() {
this.$wrapper.find('.js-total-weight').html(
HelperInstances.get(this).getTotalWeightString()
);
}
... lines 55 - 180
}
... lines 182 - 233
})(window, jQuery, Routing, swal);

This is awesome! Helper is still private, but now each RepLogApp instance will have its own instance of Helper in the Map.

Just to prove this is not breaking everything, refresh! Woohoo!

Playing with Garbage Collection

Time for an experiment! Go all the way to the bottom of the file. Create a new RepLogApp object... and just pass in the body tag. Copy this and repeat it three other times:

... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 233
new RepLogApp($('body'));
new RepLogApp($('body'));
new RepLogApp($('body'));
new RepLogApp($('body'));
... lines 238 - 240
})(window, jQuery, Routing, swal);

Notice that these are not being used: I'm not setting them to a variable. In other words, they are created, and then they're gone: no longer referenced by anything. Below that - and this won't make sense yet, call setTimeout(), pass it an arrow function, and inside, console.log(HelperInstances). Set that to run five seconds after we load the page:

... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 233
new RepLogApp($('body'));
new RepLogApp($('body'));
new RepLogApp($('body'));
new RepLogApp($('body'));
console.log(HelperInstances);
... lines 239 - 240
})(window, jQuery, Routing, swal);

Mysterious!?

Ok, refresh! And then wait a few seconds... we should see the Map printed with five Helper objects inside. Yep, we do! One Helper for each RepLogApp we created.

But now, back in RepLogApp, after we set the HelperInstance, simply return:

... lines 1 - 2
(function(window, $, Routing, swal) {
let HelperInstances = new Map();
class RepLogApp {
constructor($wrapper) {
this.$wrapper = $wrapper;
HelperInstances.set(this, new Helper($wrapper));
return;
... lines 12 - 29
}
... lines 31 - 181
}
... lines 183 - 240
})(window, jQuery, Routing, swal);

This is a temporary hack to show off garbage collection. Now that we're returning immediately, when we create a new RepLogApp object, it's not attaching any listeners or adding itself as a reference to anything in the code. In other words, this object is not attached or referenced anywhere in memory. Because of that, RepLogApp objects - and their Helper objects - should be eligible for garbage collection.

Now, garbage collection isn't an instant process - it takes places at intervals, and it's up to your JavaScript engine to worry about that. But if you're using Chrome, you can force garbage collection! On the timeline tab, you should see a little garbage icon. Try this: refresh! Quickly click the "collect garbage" button, and then see what prints in the console.

Ok, so HelperInstances still has 5 objects inside. In other words, the Helper objects were not garbage collected. Why? Because they are still being referenced in the code... by the Map itself!

Now, change the Map to a WeakMap:

... lines 1 - 2
(function(window, $, Routing, swal) {
let HelperInstances = new WeakMap();
... lines 6 - 240
})(window, jQuery, Routing, swal);

Go back and repeat the dance: refresh, hit the garbage icon, and then go to the console. Woh! Check this out! The WeakMap is empty. Remember, this is its superpower! Since none of the RepLogApp objects are being referenced in memory anymore, both those and their Helper instances are eligible for garbage collection. When you use Map, it prevents this: simply being inside of the Map counts as a reference. With WeakMap that doesn't happen.

Ok, I know, this was still pretty darn advanced. So you may or may not have this use case. But this is when you will see WeakMap used instead of Map. For us it means we should use Map in normal situations... and WeakMap only if we find ourselves with this problem.

Get rid of all our debug code:

... lines 1 - 2
(function(window, $, Routing, swal) {
let HelperInstances = new WeakMap();
class RepLogApp {
constructor($wrapper) {
... lines 9 - 28
}
... lines 30 - 180
}
... lines 182 - 233
})(window, jQuery, Routing, swal);

And our page is happy again!

Leave a comment!

0
Login or Register to join the conversation
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