Let's find out about WeakMap


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.

What is 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.

Untitled.png

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. 👀 ) Untitled 1.png

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

Characteristics of WeakMap constructor

  • WeakMap is a property of the global object.
  • It is not implemented to be called as a general function, and an error occurs when it is called as a general function.
  • The constructor must always be called with the new keyword.
  • It has an internal slot [[Prototype]] and its value is Function.prototype. (WeakMap.prototype.constructor.proto === Function.prototype)
  • It can be used for inheritance via the extends keyword.

Methods of WeakMap

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.

WeakMap.prototype.delete(key)

Removes all values mapped to key. WeakMap.prototype.has(key) will return false afterwards.

WeakMap.prototype.get(key)

Returns the value mapped to key, or undefined if there is no mapped value.

WeakMap.prototype.has(key)

Returns a boolean value that asserts whether a value mapped to key in the WeakMap instance object exists or not.

WeakMap.prototype.set(key, value)

Sets the value (value) mapped to the key in the WeakMap instance object, and returns the WeakMap instance object.

WeakMap.prototype.clear()

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)

Example of using WeakMap

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.

Caching

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.

Untitled 2.png

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.

Untitled 3.png

Custom event

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. Untitled 4.png

If we use WeakMap, we can see that the objects have been collected by the garbage collector. Untitled 5.png

Private field

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.

  • In terms of information hiding, you can use a value only if you know both the WeakMap instance and the class A's instance.
  • In terms of memory leak prevention, if an instance reference of class A does not exist except for the WeakMap instance, the memory is automatically reclaimed.

For more details, refer to the link.

Summary

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.

Jinwoo Lee2021.09.01
Back to list