Hiding Implementation Details with ECMAScript 6 WeakMaps

January 13th, 2014

WeakMaps are a new feature in ECMAScript 6 that, among many other things, gives us a new technique to hide private implementation data and methods from consumers of the public API we choose to expose.

Overview

Here is what the basics look like:

const privates = new WeakMap();

function Public() {
  const me = {
    // Private data goes here
  };
  privates.set(this, me);
}

Public.prototype.method = function () {
  const me = privates.get(this);
  // Do stuff with private data in `me`...
};

module.exports = Public;

Two things to take note of:

  1. Private data and methods belong inside the object stored in the privates WeakMap.

  2. Everything exposed on the instance and prototype is public; everything else is inaccessible from the outside world because privates isn't exported from the module.

In the Firefox Developer Tools, Anton Kovalyov used this pattern in our editor module. We use CodeMirror as the underlying implementation for our editor, but do not want to expose it directly to consumers of the editor API. Not exposing CodeMirror allows us to upgrade it when there are backwards incompatible releases or even replace CodeMirror with a different editor without the fear of breaking third party addons that have come to depend on older CodeMirror versions.

const editors = new WeakMap();

// ...

Editor.prototype = {

  // ...

  /**
   * Mark a range of text inside the two {line, ch} bounds. Since the range may
   * be modified, for example, when typing text, this method returns a function
   * that can be used to remove the mark.
   */
  markText: function(from, to, className = "marked-text") {
    let cm = editors.get(this);
    let text = cm.getRange(from, to);
    let span = cm.getWrapperElement().ownerDocument.createElement("span");
    span.className = className;
    span.textContent = text;

    let mark = cm.markText(from, to, { replacedWith: span });
    return {
      anchor: span,
      clear: () => mark.clear()
    };
  },

  // ...

};

module.exports = Editor;

In the editor module, editors is the WeakMap mapping public Editor instances to private CodeMirror instances.

Why WeakMaps?

WeakMaps are used instead of normal Maps or the combination of instance IDs and a plain object so that we neither hold onto references and leak memory nor need to introduce manual object lifetime management. For more information, see the "Why WeakMaps?" section of the MDN documentation for WeakMaps.

Compared to Other Approaches

Prefixing Private Members with an Underscore

This habit comes from the world of Python, but is pretty well spread through JS land.

function Public() {
  this._private = "foo";
}

Public.prototype.method = function () {
  // Do stuff with `this._private`...
};

It works just fine when you can trust that the consumers of your API will respect your wishes and ignore the "private" methods that are prefixed by an underscore. For example, this works peachy when the only people consuming your API are also on your team, hacking on a different part of the same app.

It completely breaks down when third parties are consuming your API and you want to move quickly and refactor without fear.

Closing Over Private Data in the Constructor

Alternatively, you can close over private data in your constructor or just define functions which return objects with function members that close over private variables.

function Public() {
  const closedOverPrivate = "foo";
  this.method = function () {
    // Do stuff with `closedOverPrivate`...
  };
}

// Or

function makePublic() {
  const closedOverPrivate = "foo";
  return {
    method: function () {
      // Do stuff with `closedOverPrivate`...
    }
  };
}

This works perfectly as far as information hiding goes: the private data is inaccessible to API consumers.

However, you are creating new copies of every method for each instance that you create. This can balloon your memory footprint if you are instantiating many instances, which can lead to noticeable GC pauses or even your app's process getting killed on mobile platforms.

ES6 Symbols

Another language feature coming in ECMAScript 6 is the Symbol primitive type and it is designed for the kind of information hiding we have been discussing.

const privateFoo = Symbol("foo");

function Public() {
  this[privateFoo] = "bar";
}

Public.prototype.method = function () {
  // Do stuff with `this[privateFoo]`...
};

module.exports = Public;

Unfortunately, Symbols are only implemented in V8 (behind the --harmony or --harmony_symbols flags) at the time of writing, but this is temporary.

More problematic is the fact that you can enumerate the Symbols in an object with the Object.getOwnPropertySymbols and Object.getOwnPropertyKeys functions. Because you can enumerate the Symbols in an object, a determined third party could still access your private implementation.

Conclusion

The WeakMap privates pattern is the best choice when you really need to hide private implementation details from public API consumers.

References

« Previous Entry

Next Entry »


Recent Entries

Naming `eval` Scripts with the `//# sourceURL` Directive on December 5th, 2014

wu.js 2.0 on August 7th, 2014

Come work with me on Firefox Developer Tools on July 8th, 2014

Debugging Web Performance with Firefox DevTools - Velocity 2014 on June 26th, 2014

Beyond Source Maps on March 12th, 2014

Memory Tooling in Firefox Developer Tools in 2014 on March 4th, 2014

Hiding Implementation Details with ECMAScript 6 WeakMaps on January 13th, 2014

Re-evaluate Individual Functions in Firefox Developer Tools' Scratchpad on November 22nd, 2013

Testing Source Maps on October 2nd, 2013

Destructuring Assignment in ECMAScript 6 on August 15th, 2013

Creative Commons License

Fork me on GitHub