Achieving better internal state encapsulation in JavaScript

Vesko Karaganev
Frontend Weekly
Published in
5 min readApr 4, 2017

Achieving encapsulation and privacy has always been an known issue in JavaScript.

When you do a quick search on “Private data in JavaScript”, you will see that you have a few different options. The following article summarises these methods and offers a new, arguably better method.

  1. Encapsulate the private data in the object’s constructor and add all the methods that work with that data to the object’s constructor.
function Counter() { 
var count = 0;
this.get = function() {
return count;
};
this.increment = function() {
return count += 1;
};
}

By defining the private variable inside of the object’s constructor and then referencing it only from the closures created in the constructor, you are making everyone except the explicitly assigned member functions unable to see and use the ‘counter’ variable.

What is not so good about this method, is the fact that every time a new instance of the object is created, a new instance of all of it’s internal methods is also made. If you think that creating multiple closures per object is cheap — you are wrong. In this way each of your objects may use multiple times more memory that when simply adding it’s methods to the object’s prototype. Which leads us to the second way of doing things.

2. Adding the private data to the object’s public properties and relying on naming convention and the kindness of your API’s users.

function Counter() {
this._count = 0;
}
Counter.prototype.get = function () {
return this._count;
}
Counter.prototype.increment = function() {
return this._count += 1;
}

This second method is in ways both better and worse than the previous one. Starting with the good things. The object’s methods are now defined on the object’s prototype which now makes every object much lighter on memory and processor usage.

What is worse about this method is that your object’s data isn’t actually private anymore. Even though the starting-with-underscore naming convention is very popular, there might still be some users who will be misuse your object’s state which besides everything else, may also make it harder for you to update your object’s interface.

3. The not so popular WeakMap-based method.

// As this method requires some other state bookkeeping 
// we will wrap it's internals in an IIFE (Immediately-Invoked Function Expression).
var Counter = (function () {
// Create a new WeakMap, which maps the object's
// instance to it's private data.
var privateData = new WeakMap();
function Counter() {
// Set the initial state of the object.
privateData.set(this, {
count: 0,
});
}
Counter.prototype.get = function () {
// Find the data that matches this instance
// and return it's the count.
return privateData.get(this).count;
}
Counter.prototype.increment = function() {
return privateData.get(this).count+= 1;
}
return Counter;}());

The way this works is by adding the private data of every object to a captured WeakMap and then accessing that same data using the object’s reference.

A pro of this method is that it does create better encapsulation than the previous method as it does not depend on the user’s willingness to follow the author’s naming convention.

Time for the cons. This method requires searching for a reference in the WeakMap every time a function wants to use the object’s internal state. Which, as you can imagine, does not come for free. Another bad point is given to the fact that WeakMaps, as of writing this, were added to the standard quite recently, which means that not all browsers support them. There is always the option of using a polyfill, but in that case, keep in mind that all the current polyfills will prevent your object’s from ever being garbage collected, which means that they will stay in the user’s memory until he leaves the current window.

4. Time for the good stuff. This last method combines the best parts of the first two methods and none of their weaknesses.

// A convenience function, that creates an accessor named _getPrivateData 
// which takes a matching key to expose it's data and adds it to the provided object.
function initPrivateData(self, privateKey, data) {
Object.defineProperty(self, '_getPrivateData', {
configurable: false,
enumerable: false,
writable: false,
value: function getPrivateData(key) {
if (key !== privateKey) {
throw new Error("Illegal access");
} else {
return data;
}
},
});
}
var Counter = (function () {

// This is the private key for the Counter class.
// In JS two objects compare equal only if they are the <same instance>.
var privateKey = {};
// Get the Couter's private data by providing the private key.
function getPrivateData(self) {
return self._getPrivateData(privateKey);
}
function Counter() {
// Initialise the private data with the private key
// and some initial state.
initPrivateData(this, privateKey, {
count: 0,
});
}
Counter.prototype.get = function () {
// Get the count from the object's private data.
return getPrivateData(this).count;
}
Counter.prototype.increment = function() {
return getPrivateData(this).count += 1;
}
return Counter;}());

The way this works is by capturing the object’s private state, like in the first method offered, but unlike it, this method does not require all the methods that have access to that data to be declared on the instance, which again, saves memory and gives better performance.

The encapsulation is achieved by passing in a private key (an empty object) before getting the final data. If the private key is not the same on the object’s side, the access to the data is denied.

This method allows garbage collection, unlike the third method, and gives the developer a lot better and simple interface to the internals of the object that allows the object’s methods to still have access to it’s private state.

Note: All of the examples in this article are written in ES5 syntax in order for them to be more straightforward.

Here is the last fourth method, rewritten in ES6 syntax.

// in private-data.js
export default function initPrivateData(self, privateKey, data) {
Object.defineProperty(self, '_getPrivateData', {
configurable: false,
enumerable: false,
writable: false,
value: function getPrivateData(key) {
if (key !== privateKey) {
throw new Error("Illegal access");
} else {
return data;
}
},
});
}
// in counter.js
import initPrivateData from './private-data.js';
const privateKey = {};
const getPrivateData = self => self._getPrivateData(privateKey);
export default class Counter {
constructor() {
initPrivateData(this, privateKey, {
count: 0,
});
}
get() {
return getPrivateData(this).count;
}
increment() {
return getPrivateData(this).count += 1;
}
}

Another note to the reader: This is my first publication on Medium. Your support is welcome :)

--

--