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.
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!