Years have passed since ES2015 came out in June 2015. While writing the previous weekly picks, I thought, "Is there any ES2015 syntax I'm not using, like Proxy
or Reflect
? 🤔". The first things that came to mind were WeakMap
and WeakSet
. I became interested in weak references, so I translated the related content in the last weekly pick (link). My interest on the topic grew stronger, so this time I'm writing about WeakMap
.
A WeakMap
object is a collection of key/value pairs in which keys are weakly referenced. The keys must be objects. A primitive value cannot be a key. If you add a primitive value as a key, you will get an error Uncaught TypeError: Invalid value used as weak map key
.
WeakMap
provides a method for checking whether there is a value for a specific key, but it does not provide a way to enumerate the objects held as keys. And as is well known, objects used as keys in WeakMap
are subject to garbage collection. If a reference to an object in the program does not exist except WeakMap
, the object is garbage collected.
Let's say we have a function that returns a long array using closure, like this:
// refs:
// https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots/
function createLargeClosure() {
const largeStr = new Array(1000000).join('x');
const lc = function largeClosure() {
return largeStr;
};
return lc;
}
First, let's create a memory leak using Map
.
const map = new Map();
function start() {
const timer = setInterval(() => {
const lc = createLargeClosure();
map.set(lc, '##');
}, 1000);
setTimeout(function () {
clearInterval(timer);
}, 5001);
}
I checked the memory allocation status with DevTools.
Now, if we run the same code after replacing the collection object with new WeakMap()
instead of new Map()
, we can see that the memory is deallocated as follows. (Actually, I've only heard of it and it's the first time I've checked it like this, so I was amazed. 👀 )
If you are unfamiliar with this snapshot because you're not familiar with DevTools, I recommend you watch the "Gentle Walkthrough of Chrome DevTools (link)" or read the Chrome documentation (link).
WeakMap
is a property of the global object.new
keyword.[[Prototype]]
and its value is Function.prototype
.
(WeakMap.prototype.constructor.proto === Function.prototype
)extends
keyword.There are only four WeakMap
methods like below. As mentioned earlier, methods related to enumeration are not supported because of garbage collection. According to the ECMAScript specification, implementations of WeakMap
may have a delay between the time the key/value pair becomes inaccessible and the time the key/value pair is removed from the WeakMap
. Therefore, operations involving enumeration of the entire key/value pairs cannot be performed.
Removes all values mapped to key
. WeakMap.prototype.has(key)
will return false
afterwards.
Returns the value mapped to key
, or undefined
if there is no mapped value.
Returns a boolean
value that asserts whether a value mapped to key
in the WeakMap
instance object exists or not.
Sets the value (value
) mapped to the key
in the WeakMap
instance object, and returns the WeakMap
instance object.
The clear()
method is deprecated in all browsers except IE due to security issues. Only those who have both WeakMap
and keys should be able to observe or affect key/value mapping of WeakMap
. By using clear()
, however, someone who only has WeakMap
can affect WeakMap
and key/value mapping. (reference)
Now that the brief introduction of WeakMap
is over, let's look at an example of how to use it. Generally, it is used for code that is prone to memory leaks.
WeakMap
facilitates memoization. Memoization is a technique to cache the result of expensive calculations and return the cached result when the same input value is received.
function createLargeClosure() {
const largeObj = {
a: 1,
b: 2,
str: new Array(1000000).join('x')
};
const lc = function largeClosure() {
return largeObj;
};
return lc;
}
const memo = new WeakMap();
function memoize(obj) {
if (memo.has(obj)) {
console.log('Get cached result');
return memo.get(obj);
}
const compute = obj.a + obj.b;
console.log('Set computed result to caching map');
memo.set(obj, compute);
return compute;
}
function start() {
const lcObj = createLargeClosure();
const timer = setInterval(() => {
memoize(lcObj());
}, 1000);
setTimeout(function () {
clearInterval(timer);
}, 5001);
}
If memo
is created with Map
, the memory allocation status checked with the DevTools is as follows.
Now, if we change the cache object from Map
to WeakMap
, we can see that the object remaining on the figure above has been removed and the blue gauge went down slightly in the upper bar chart as the memory was deallocated.
In simple vanilla projects, you will often need a custom event object. There is a way to mix-in an event object to the instance like code snippet of TOAST UI. However, if you simply implment the event object as a singleton, WeakMap
can help prevent memory leaks.
The following code registers an event handler with targetObject
as a key and executes it.
class EventEmitter {
constructor() {
this.targets = new WeakMap();
}
on(targetObject, handlers) {
if (!this.targets.has(targetObject)) {
this.targets.set(targetObject, {});
}
const targetHandlers = this.targets.get(targetObject);
Object.keys(handlers).forEach(handlerName => {
targetHandlers[handlerName] = targetHandlers[handlerName] || [];
targetHandlers[handlerName].push(handlers[handlerName].bind(targetObject));
});
}
fire(targetObject, handlerName, args) {
const targetHandlers = this.targets.get(targetObject);
if (targetHandlers && targetHandlers[handlerName]) {
targetHandlers[handlerName].forEach(handler => handler(args));
}
}
}
When the code below is executed, the memory of the user
and handlers
objects is collected by the garbage collector because user
is registered as a key in WeakMap
.
const emitter = new EventEmitter();
function start() {
const user = {
name: 'John'
};
const handlers = {
sayHello: function() {
console.log(`Hello, my name is ${this.name}`);
},
sayGoodBye: function() {
console.log(`Good bye, my name is ${this.name}`);
}
};
emitter.on(user, handlers);
const timer = setInterval(() => {
emitter.fire(user, 'sayHello');
emitter.fire(user, 'sayGoodBye');
}, 1000);
setTimeout(function () {
clearInterval(timer);
}, 5001);
}
The figure below shows the situation in which user
and handlers
are not collected when EventEmitter
is implemented with Map
.
If we use WeakMap
, we can see that the objects have been collected by the garbage collector.
This can be easily found in Babel's transpiling results. The following code specifies the access modifier with the #
keyword.
class A {
#privateFieldA = 1;
#privateFieldB = 'A';
}
The following shows the actual result after transpiling. You can test this code in Babel's Try it out (link).
"use strict";
var _privateFieldA = /*#__PURE__*/new WeakMap();
var _privateFieldB = /*#__PURE__*/new WeakMap();
class A {
constructor() {
_privateFieldA.set(this, {
writable: true,
value: 1
});
_privateFieldB.set(this, {
writable: true,
value: 'A'
});
}
}
The reason for using WeakMap
for private fields is as follows.
WeakMap
instance and the class A's instance.WeakMap
instance, the memory is automatically reclaimed.For more details, refer to the link.
In conclusion, WeakMap
is a data structure that has an advantage in memory leak management by keeping weak references, although it does not support enumeration methods like Map
.
While writing this weekly pick, I intentionally wrote complicated code to show an example of a situation where memory is not reclaimed, but this was to show when the garbage collector can and cannot reclaim memory.
In addition to being used for Vue 3 reactivity, WeakMap
was used in an example of how to use _.memoize
in lodash
, which shows that it is possible to change a cache object to WeakMap
. It is also used to polyfill private access modifiers for information hiding. Since IE11 is included in the browser support scope (reference), I think it can be usefully used at the framework or library level.
In fact, most projects these days are implemented based on frameworks, so you won't use it often in general situations. But if you need a data structure that has an object in the project as an identifier, you might want to consider using WeakMap
before using regular objects or Map
right away.