Qubyte Codes

Private data for JS classes with WeakMap

Published

Private data has always been awkward in JavaScript. It's particularly difficult when it comes to constructors, and with ES2015 recently published, classes too. Let's say we have an example class, exported by an ES2015 module:

export default class Example {
  constructor() {
    this._privateDatum = Math.random();
  }

  log() {
    console.log(this._privateDatum);
  }
}

In the constructor, a field with some private data, _privateDatum is appended (the value is a placeholder for illustration). The initial underscore in the name is a common convention and is meant to tell developers using the class that they shouldn't touch or look at that field. Why should this be private? Private stuff is subject to change without your users needing to know about it. This field could be renamed or go away completely if you refactor, without affecting the public API. So what's the problem?

You can't trust your users!

This isn't meant as an insult. Your users are cunning, and if they can solve a problem without filing an issue or raising a pull request, they probably will. They have deadlines after all... If your class gets very popular, it becomes inevitable that someone is going to use your private-by-convention field to hack together a solution to a problem they're having, and you'll break their code when you change it. Changes to your public API should be clearly indicated by changes to the version number and updated documentation. The inner workings of your code on the other hand, including private data, are subject to dramatic change at any time.

The solution is to hide the private data, removing the temptation. You can do this using a closure. Consider:

export default class Example {
  constructor() {
    const privateDatum = Math.random();

    this.log = function () {
      console.log(privateDatum);
    }
  }
}

In this example, the private data is now assigned to a variable in the constructor. Since the variable is not returned, nothing outside the constructor will have access to it. The pain now is that the log method has to be attached to the instance inside the constructor, so that it can have access to the variable. It's a real shame to lose the nice method syntax. It also make an individual log method for each instance, which means objects will each use more memory.

This is where WeakMap comes in. An instance of WeakMap has keys which are objects of some kind, and values which can be whatever you like. WeakMap instances are especially good, since if they are the last thing to hold a reference to an object (as a key), then the JS engine is allowed to garbage collect it. This means the risk of memory leaks is lessened. You could simulate most aspects of WeakMap using existing structures like arrays, but that would always result in a memory leak, since the garbage collector thinks those objects are in use and cannot clear them up. The final example below shows what this looks like:

const privateDatum = new WeakMap();

export default class Example {
  constructor() {
    privateDatum.set(this, Math.random());
  }

  log() {
    console.log(privateDatum.get(this));
  }
}

The keys of privateDatum are the instances of the example class. If nothing else holds a reference to an instance of the example class, the garbage collector doesn't count the reference in privateDatum and can clear it up! Since the instances are keys, this can be used in any method to access the private data. The privateDatum variable hidden by the module, so the user will have no access to it!

This approach can be used with constructor functions and methods appended to the prototype too. The following constructor produces objects with similar behaviour to those produced by the class in the previous example:

const privateDatum = new WeakMap();

export default function Example() {
  privateDatum.set(this, Math.random());
}

Example.prototype.log = function () {
  console.log(privateDatum.get(this));
};

The good news is that WeakMap is one of the most well supported features of ES2015. With the exception of IE Mobile and Opera Mobile, all current versions of major browsers support the functionality in this post. See the compatibility table. If you're using a maintained version of Node, you're good to go!

Addendum

It's still possible to gain access to private data stored in a WeakMap by patching WeakMap.prototype.set or WeakMap.prototype.get. This should absolutely never be done! Along with the usual reasons to not modify the prototype of a built in constructor, modifying WeakMap risks undoing the whole reason for using it in the first place. By monitoring objects used as the keys of a WeakMap, references can be created and the garbage collector may not be able to clean up after you. That said, patching can be done. If you want to avoid that risk, you can Object.freeze both WeakMap and WeakMap.prototype before any other code runs.

Backlinks