This post is part of a series of ES2015 posts. We'll be covering new JavaScript functionality every week!

One of the new features of ECMAScript 2015 is the WeakMap. It has several uses, but one of the most promoted is to store properties that can only be retrieved by an object reference, essentially creating private properties. We'll show several different implementation approaches and compare it in terms of memory usage and performance with a 'public' properties variant.

A classic way

Let's start with an example. We want to create a Rectangle class that is provided the width and height of the rectangle when instantiated. The object provides an area() function that returns the area of the rectangle. The example should make sure that the width and height cannot be accessed directly, but they must be stored both.

First, for comparison, a classic way of defining 'private' properties using the ES2015 class syntax. We simply create properties with an underscore prefix in a class. This of course doesn't hide anything, but a user knows that these values are internal and the user shouldn't let code depend on its behaviour.

class Rectangle {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }

  area() {
    return this._width * this._height;
  }
}

We'll do a small benchmark. Let's create 100.000 Rectangle objects, access the area() function and benchmark the memory usage and speed of execution. See the end of this post on how this was benchmarked. In this case, Chrome took ~49ms and used ~8Mb of heap.

WeakMap implementation for each private property

Now, we introduce a WeakMap in the following naive implementation that uses a WeakMap per private property. The idea is to store a value using the object itself as key. In this way, only the accessor of the WeakMap can access the private data, and the accessor should be of course only the instantiated class. A benefit of the WeakMap is the garbage collection of the private data in the map when the original object itself is deleted.

const _width = new WeakMap();
const _height = new WeakMap();

class Rectangle {
  constructor (width, height) {
    _width.set(this, width);
    _height.set(this, height);
  }

  area() {
    return _width.get(this) * _height.get(this);
  }
}

To create 100.000 Rectangle objects and access the area() function, Chrome took ~152ms and used ~22Mb of heap on my computer. We can do better.

Faster WeakMap implementation

A better approach would be to store all private data in an object for each Rectangle instance in a single WeakMap. This can reduce lookups if used properly.

const map = new WeakMap();

class Rectangle {
  constructor (width, height) {
    map.set(this, {
      width: width,
      height: height
    });
  }

  area() {
    const hidden = map.get(this);
    return hidden.width * hidden.height;
  }
}

This time, Chrome took ~89ms and used ~21Mb of heap. As expected, the code is faster because there's a set and get call less. Interestingly, memory usage is more or less the same, even though we're storing less object references. Maybe a hint on the internal implementation of a WeakMap in Chrome?

WeakMap implementation with helper functions

To improve the readability of above code, we could create a helper lib that should export two functions: initInternal and internal, in the following fashion:

const map = new WeakMap();
let initInternal = function (object) {
  let data = {};
  map.set(object, data);
  return data;
};
let internal = function (object) {
  return map.get(object);
};

Then, we can initialise and use the private vars in the following fashion:

class Rectangle {
  constructor(width, height) {
    const int = initInternal(this);
    int.width = width;
    int.height = height;
  }

  area() {
    const int = internal(this);
    return int.width * int.height;
  }
}

For the above example, Chrome took ~108ms and used ~23Mb of heap. It is a little bit slower than the direct set/get call approach, but is faster than the separate lookups.

Conclusion

  • The good: real private properties are now possible
  • The bad: it costs more memory and degrades performance
  • The ugly: we need helper functions to make the syntax okay-ish

WeakMap comes at both a performance as well a memory usage cost (at least as tested in Chrome). Each lookup for an object reference in the map takes time, and storing data in a separate WeakMap is less efficient than storing it directly in the object itself. A rule of thumb is to make sure to do as few lookups as necessary. For your project it will be a tradeoff to store private properties with a WeakMap versus lower memory usage and higher performance. Make sure to test your project with different implementations, and don't fall into the trap of micro-optimising too early.

Test reference

Make sure to run Chrome with the following parameters: --enable-precise-memory-info --js-flags="--expose-gc" - this enables detailed heap memory information and exposes the gc function to trigger garbage collection.

Then, for each implementation, the following code was run:

const heapUsed = [];
const timeUsed = [];

for (let i = 1; i <= 50; i++) {
  const instances = [];
  const areas = [];

  gc();
  const t0 = performance.now();
  const m0 = performance.memory.usedJSHeapSize;

  for (let j = 1; j <= 100000; j++) {
    var rectangle = new Rectangle(i, j);
    instances.push(rectangle);
    areas.push(rectangle.area());
  }

  const t1 = performance.now();
  const m1 = performance.memory.usedJSHeapSize;

  heapUsed.push(m1 - m0);
  timeUsed.push(t1 - t0);
}

var sum = function (old, val) {
  return old + val;
};
console.log('heapUsed', heapUsed.reduce(sum, 0) / heapUsed.length);
console.log('timeUsed', timeUsed.reduce(sum, 0) / heapUsed.length);