What Are the Actual Uses of Es6 Weakmap

What are the actual uses of ES6 WeakMap?

Fundamentally

WeakMaps provide a way to extend objects from the outside without interfering with garbage collection. Whenever you want to extend an object but can't because it is sealed - or from an external source - a WeakMap can be applied.

A WeakMap is a map (dictionary) where the keys are weak - that is, if all references to the key are lost and there are no more references to the value - the value can be garbage collected. Let's show this first through examples, then explain it a bit and finally finish with real use.

Let's say I'm using an API that gives me a certain object:

var obj = getObjectFromLibrary();

Now, I have a method that uses the object:

function useObj(obj){
doSomethingWith(obj);
}

I want to keep track of how many times the method was called with a certain object and report if it happens more than N times. Naively one would think to use a Map:

var map = new Map(); // maps can have object keys
function useObj(obj){
doSomethingWith(obj);
var called = map.get(obj) || 0;
called++; // called one more time
if(called > 10) report(); // Report called more than 10 times
map.set(obj, called);
}

This works, but it has a memory leak - we now keep track of every single library object passed to the function which keeps the library objects from ever being garbage collected. Instead - we can use a WeakMap:

var map = new WeakMap(); // create a weak map
function useObj(obj){
doSomethingWith(obj);
var called = map.get(obj) || 0;
called++; // called one more time
if(called > 10) report(); // Report called more than 10 times
map.set(obj, called);
}

And the memory leak is gone.

Use cases

Some use cases that would otherwise cause a memory leak and are enabled by WeakMaps include:

  • Keeping private data about a specific object and only giving access to it to people with a reference to the Map. A more ad-hoc approach is coming with the private-symbols proposal but that's a long time from now.
  • Keeping data about library objects without changing them or incurring overhead.
  • Keeping data about a small set of objects where many objects of the type exist to not incur problems with hidden classes JS engines use for objects of the same type.
  • Keeping data about host objects like DOM nodes in the browser.
  • Adding a capability to an object from the outside (like the event emitter example in the other answer).

Let's look at a real use

It can be used to extend an object from the outside. Let's give a practical (adapted, sort of real - to make a point) example from the real world of Node.js.

Let's say you're Node.js and you have Promise objects - now you want to keep track of all the currently rejected promises - however, you do not want to keep them from being garbage collected in case no references exist to them.

Now, you don't want to add properties to native objects for obvious reasons - so you're stuck. If you keep references to the promises you're causing a memory leak since no garbage collection can happen. If you don't keep references then you can't save additional information about individual promises. Any scheme that involves saving the ID of a promise inherently means you need a reference to it.

Enter WeakMaps

WeakMaps mean that the keys are weak. There are no ways to enumerate a weak map or to get all its values. In a weak map, you can store the data based on a key and when the key gets garbage collected so do the values.

This means that given a promise you can store state about it - and that object can still be garbage collected. Later on, if you get a reference to an object you can check if you have any state relating to it and report it.

This was used to implement unhandled rejection hooks by Petka Antonov as this:

process.on('unhandledRejection', function(reason, p) {
console.log("Unhandled Rejection at: Promise ", p, " reason: ", reason);
// application specific logging, throwing an error, or other logic here
});

We keep information about promises in a map and can know when a rejected promise was handled.

What's the difference between ES6 Map and WeakMap?

From the very same page, section "Why Weak Map?":

The experienced JavaScript programmer will notice that this API could
be implemented in JavaScript with two arrays (one for keys, one for
values) shared by the 4 API methods. Such an implementation would have
two main inconveniences. The first one is an O(n) search (n being the
number of keys in the map). The second one is a memory leak issue.
With manually written maps, the array of keys would keep references to
key objects, preventing them from being garbage collected. In native
WeakMaps, references to key objects are held "weakly", which means
that they do not prevent garbage collection in case there would be no
other reference to the object.

Because of references being weak, WeakMap keys are not enumerable
(i.e. there is no method giving you a list of the keys). If they were,
the list would depend on the state of garbage collection, introducing
non-determinism.

[And that's why they have no size property as well]

If you want to have a list of keys, you should
maintain it yourself. There is also an ECMAScript
proposal
aiming at introducing simple sets and maps which would not use weak
references and would be enumerable.

‐ which would be the "normal" Maps. Not mentioned at MDN, but in the harmony proposal, those also have items, keys and values generator methods and implement the Iterator interface.

When would you use a Map over a WeakMap when you have objects as keys?

Consider an implementation for modeling graphs. Lets assume nodes of the graph can be user defined objects. The graph implementation needs to store those nodes but also associate them with other data (such as edges (think of an adjacency "dictionary")). We would need a Map since we have to be able to iterate over all nodes as well (that's e.g. what networkx.github.io does (in Python) and my JS port of it)

And even if we didn't need to iterate over the nodes, we probably wouldn't want them to be garbage collected if there are no other references to them, since that would silently destroy the graph.

- Felix Kling

What are ECMAScript 6 WeakMaps?

WeakMap

WeakMaps basically allow you to have a HashTable with a key that isn't a String.

So you can set the key to be, i.e. [1] and then can say Map.get([1])

Example from the MDN:

var wm1 = new WeakMap(),
wm2 = new WeakMap();
var o1 = {},
o2 = function(){},
o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // a value can be anything, including an object or a function
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // keys and values can be any objects. Even WeakMaps!

wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, because there is no value for o2 on wm2
wm2.get(o3); // undefined, because that is the set value

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (even if the value itself is 'undefined')

wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false

The reason for its existance is:

in order to fix a memory leak present in many uses of weak-key tables.

Apparently emulating weakmaps causes memory leaks. I don't know the details of those memory leaks.

WeakMap showing different results for same code

The difference will depend on when the garbage collector runs in comparison to when you click the WeakMap item in the console to expand it. Take careful note of the warning there:

Sample Image

The items the console shows that exist in the WeakMap are the ones that were in it right when you clicked - not when the console.log line ran.

If the garbage collector ran before you clicked, the x object would be GC'd, and the WeakMap would appear empty.

If the garbage collector did not run before you clicked, the x object would not be GC'd, and the WeakMap would appear populated.

Is garbage collection unpredictable?

In general, yes. Best not to rely on it.

ES6 WeakMap Class encapsulation

The problem with an ordinary variable like my_var is that it will only save data for a single instantiation of the class:

const Test = (function encapsulation() {  let my_var = 'My secret info';
class Test { constructor(param) { my_var = param; } getInfo() { return my_var; } } return Test;})();
const t1 = new Test('foo');const t2 = new Test('bar');console.log(t1.getInfo());// the above returns 'bar'... uh oh, but we passed in 'foo' to `t1`! Our data is lost!console.log(t2.getInfo()); // 'bar'

Weakmap memory leak

It seemed like the problem was not a real problem. In fact, I was using the console to test this. I figured out that, also reading somewhere on StackOverflow, that using console does not allow some object to be garbage collected. This happens both on V8 and SpiderMonkey.

Executing the code chunks above all together, also with this:

myObject = null;
(window || global).gc(true); // Valid only for V8

creates a Weakmap and successfully empties it.

So what I did was to execute line-by-line, close the console and reopen it. I printed the WeakMap content and it wasn't available anymore. (This, both on Chrome and Firefox)
It might be needed to open and close different times to see the garbage collection done.

To force the garbage collection on Firefox, I opened a new tab on about:memory and tried to click the button GC.

WeakMap inverted

You can solve it by using WeakRef and FinalizationRegistry.

Here is an example written in TypeScript:

class InvertedWeakMap<K extends string | symbol, V extends object> {
_map = new Map<K, WeakRef<V>>()
_registry: FinalizationRegistry<K>

constructor() {
this._registry = new FinalizationRegistry<K>((key) => {
this._map.delete(key)
})
}

set(key: K, value: V) {
this._map.set(key, new WeakRef(value))
this._registry.register(value, key)
}

get(key: K): V | undefined {
const ref = this._map.get(key)
if (ref) {
return ref.deref()
}
}

has(key: K): boolean {
return this._map.has(key) && this.get(key) !== undefined
}
}

async function main() {
const map = new InvertedWeakMap()
let data = { hello: "world!" } as any
map.set("string!", data)

console.log('---before---')
console.log(map.get("string!"))
console.log(map.has("string!"))

data = null
await new Promise((resolve) => setTimeout(resolve, 0))
global.gc() // call gc manually

console.log('---after---')
console.log(map.get("string!"))
console.log(map.has("string!"))
}

main()

It must be run with the --expose-gc option in the node.js environment.

WeakMap implementation in EcmaScript5?

It took me a while to grok the code, but then it hit me: the key itself is used to store a reference to the value.

For example, several layers into set it does

defProp(obj, globalID, { value: store });

where defProp has been defined to be Object.defineProperty, obj is the key, globalID is a guid and store is a storage object that contains the value.

Then down in get it looks up the value with

obj[globalID];

This is very clever. The WeakMap doesn't actually contain a reference to anything (weak or otherwise)-- it just sets up a policy of where to secretly store the value. The use of Object.defineProperty means that you won't accidentally discover the value storage-- you have to know the magic guid to look it up.

Since the key directly refers to the value (and the WeakMap doesn't refer to it), when all references to the key are gone, it gets GCed like normal.



Related Topics



Leave a reply



Submit