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.