Hello 2018! It has been 3 years since we first saw Javascript 2015 (aka ES6). During this time most of us have focused on the cosmetic changes like Arrow =>
Functions or the fancy destructing operator โฆ
.
Everyone needs something exciting like the upcoming fancy |>
pipe operator. Who cares if ES6 also added things like WeakMap
, WeakSet
, Iterables
, Map
or Set
. Even looking at this thing called WeakMap
, feels so depressing ๐.
Keeping the sarcasm aside, let us talk about WeakMaps
๐.
Why you would need something Weak
I have to agree the name WeakMap
is definitely a misnomer. If it were me I would have named it SuperMap. Before we get into definitions, let us actually take a moment and understand why we need WeakMap
's in our apps.
Imagine itโs 1990 ๐ก and you create an app of all the countries ๐ present at that time.
var USSR = {
name: 'Soviet Union',
capital: 'Moscow',
..
..
}
var countries = [ Afganishtan, Albania, Algeria, ..., USSR, ..., Zimbabwe ]
A user can click any country and get detailed information which also includes the area of the country. Below is a hypothetical area calculation function.
async function calcArea(country) {
const boundaries = await fetch(country);
area = calculateArea(country, boundaries); // takes a long time
return area;
}
Caching the Area
Every time a user clicks a country you calculate the area. But we have a problem! If a user clicks a country multiple times you have to repeat this enormous asynchronous calculation, which is something we should totally avoid. There are generally two ways to solve this kind of problem.
- Debounce the function
- Cache the function
Debouncing is a peaceful way to calm down multiple aggressive invocations in a short interval of time. (Imagine an impatient user clicking the refresh button multiple times). Debounce allows us to only take the last invocation and discard the rest.
Since countries don't change area that often, we can simply cache the result of calcArea
.
We can use both caching and debouncing to make our application performant. Below is a generic caching function which we will use to cache calcArea
.
function cachify(fn) {
// its a good idea to hide you cache inside the closure
var cache = new Map();
return arg => {
if (cache.has(arg)) {
return cache.get(arg);
}
var computed = fn(arg);
cache.set(arg, computed);
return computed;
};
}
cachedCalcArea = cachify(calcArea);
cachedCalcArea(USSR); // goes and computes the area
cachedCalcArea(USSR); // already computed, returns the cached area
Great! We made some serious performance improvements.
But we have another problem, USSR
just broke into 15 new countries. This would mean we remove USSR and add the newly formed countries to our countries
array.
countries.remove(USSR);
// add the new countries
countries.add([Armenia, Azerbaijan, ...., Uzbekistan]);
Removing USSR
just from the array doesn't help, as our cache still contains USSR
and the calculated area. A naive solution would be to monkey patch our cachify
function to remove USSR, but if the world continues to break into smaller countries we have got ourselves a memory leak.
We need a smart way to clean up our cache which scales well. There are multiple ways fellow developers would approach this problem:
- Maintain a precomputed area array and keep it in sync with countries.
- Figure out some smart cache eviction like LRU, time-based, etc.
Precomputing the area for every country seems to be a waste of computation, as most of the users won't ever be seeing every country.
We can use a smart caching strategy like Least Recently Used caching, this caching automatically removes the entry which is least recently used. But we aren't running out of memory with 160+ countries and LRU doesn't seem all that magical and seamless.
What about WeakMap?
WeakMap
is the missing jigsaw piece for our caching problem. It automatically removes* any unused references from it.
"The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced. The keys must be objects and the values can be arbitrary values." - MDN
I like to say WeakMap is nothing but a regular Map with dementia. It is a very forgiving data structure, it will forget things which no longer matter. (We should be like that too :P)
We can simply replace the Map
with WeakMap
in our caching function.
function weakCache(fn) {
var cache = new WeakMap(); // <-- Behold the Weak!
return (arg) => {
if (cache.has(arg)) {
return cache.get(arg);
}
var computed = fn(arg);
cache.set(arg, computed);
return computed;
}
}
cachedCalcArea = weakCache(calcArea);
cachedCalcArea(USSR); // cache miss
cachedCalcArea(USSR); // cache hit
Now let USSR
break into the 15 countries. We just need to take care of removing all references pointing to the USSR
obj in our app and our cachedCalcArea
function will automatically forget the USSR
entry in the cache. Hence, avoiding the memory leak!
How does it forget things?
WeakMap
works similar to a regular Map
but in order to be a forgetful version of Map it imposes these constraints:
- Primitive data type keys are not allowed (Numbers, String, null, true, etc)
- You cannot list all the values inside the WeakMap
Let us see a hypothetical example of WeakMap
- Imagine a
WeakMap
instance to be a building with thousands of ๐ช doors.
var building = new WeakMap();
- Each door has a unique key and we own a key ๐ for our
๐ช101
. Due to the constraints mentioned above the key can only be an object.
var key = {
password: '๐'
};
- We can lock/unlock our door with this key.
building.set(key, '๐ช101');
building.get(key); // ๐ช101
- Now a thief has seen our key (Its Javascript duh!) and he tries to fabricate a duplicate key.
var fake_key = {
password: '๐'
};
- Since we live in a Javascript world we clearly know even though they look same, they are not
equal
.
fake_key === key // false
- Our thief didn't read this awesome article and he tries to get into our building using his fake key only to fail :(.
building.get(fake_key); // undefined
What happens if we lose the key
As long as some variable holds the reference to our original key we are safe. But if there comes a time when no variable in the entire app is holding a reference to our key, we lose the access to our ๐ช101
.
This is exactly what powers the smart caching of a WeakMap
. If we lose the key, the GC can deduce that there is no way to access the thing associated with the key and it can safely remove it from the memory.
Note: This is the crucial difference between a WeakMap
and Map
. WeakMap
removes <key,value>
if you lose the key, but in a Map, you can simply list all the keys to find the lost key.
Coming back to USSR problem, when USSR breaks into the 15 countries and we just need to take care of removing all references to the USSR obj in our app.
countries.remove(USSR); // remove from array
USSR = undefined; // unset the variable
// at this point there is no way to get the cached area of USSR since it doesn't exist anymore
As you can see after the above steps, there is no way of accessing the USSR object in the current state of app and with this knowledge Javascript garbage collector automatically clears the memory it reserved for the area of USSR. Notice the removing happens behind the scenes and all we did was replace Map
with WeakMap
. Isn't that powerful?
WeakMap Takeaways
- Remember not to mutate the key object because in Javascript the object reference stays the same even if you mutate the object.
var obj = {name: '๐'};
weakMap.set(obj, 'animal');
obj.name = '๐โโ๏ธ';
weakMap.get(obj); // 'animal'
- WeakMap cannot accept primitive javascript values as keys. You should use
Map
if you wanna use them as your key.
weakMap.set('key', 'value'); // Error!
- Sometimes it is faster to not cache a function. If your function barely takes a millisecond to execute, you would end up slowing it down by caching.
- You can use anything as a
value
forWeakMap
/Map
. Yes even promises! - The eviction of an unsued key doesn't happen immediately. It depends on the garbage collector's mood. You shouldn't worry about this part though.
- WeakMap works great for derived state. A lot of times your application has state which can simply be derived from other state. In example below, you can see deriving a value using cached function is much more maintainable and easier to reason with.
var user = {
name: "Kushan Joshi"
}
var websites = ['Facebook', 'Github', 'Twitter', 'Dev.to', 'Medium'];
var memberOf = (user) => websites.filter(website => isUser(user));
// save the websites and keep track of it, too complicated ๐คฎ !
user.memberOf = memberOf(user);
// deriving the value using weakMaps, awesomo ๐ค!
cachedMemberOf = weakCache(memberOf); // avoid recomputing everytime
// or derive it everytime you need it
console.log(cachedMemberOf(user));
render(cachedMemberOf(user))
I really hope this article helped you in understanding WeakMaps
. I love using it with libraries like Immutable.js
or Redux
since they enforce immutability. Even if you don't use these libraries, as long as you don't mutate the object you can reap benefits from WeakMap.
I am planning on writting a Part-2
of Javascript Underdogs, let me know in the comments what Javascript feature you think is amazing but underapreciated.
If you โค๏ธ this article, please share this article to spread the words.
Reach out to me on Twitter @kushan2020.
Top comments (9)
ES6 Proxies are a great underrated feature to write about. I would certainty y like to learn some more about them.
Very informative!
One potential correction:
I think "compiler" should instead be "garbage collector".
Fixed, thanks so much for pointing that out
Nice post! Thanks
Great article!
Wonderful post, congratulations Joshi, you explain in a simple and funny way.
Great article!!!
Very clear and informative!!!
Looking forward to the new post.:...
Great article, really helped me.