WeakMap for JavaScript Private Data
Private Data in JavaScript
A common way that developers expose a public interface for interacting with private data in JavaScript is by using a closure.
const EnginePart = (() => {
// Don't want any other code to have direct access to this
const _part_info = {
part_number: "4104928345103",
weight_in_kg: 1.7
};
const Part = Object.create(null, {
getPartNumber: {
value: function () {
return _part_info.part_number;
}
}
});
return Part;
})();
Now I can create many objects with EnginePart
as their prototype. Each one will have the correct part number, and that part number only accessible through the getter method. It can't be modified by other objects.
const part_one = Object.create(EnginePart);
const part_two = Object.create(EnginePart);
part_one.part_number = "99999"
>> "99999"
part_one.getPartNumber()
>> "4104928345103"
The problem is that the closure ensures that the data in the private variable remains in memory if EnginePart
needs to garbage collected, because the private _part_info.part_number
variable reference made by the getter is never lost.
This is called a memory leak. During the execution of the application, if there are no more references to EnginePart
, the JavaScript engine will attempt to reclaim the memory being used. The closure prevents that.
WeakMap Storage
WeakMaps help solve this because the keys are weakly referenced. Therefore, if I define a WeakMap inside the closure, and make the key the object instance itself, then when that instance is garbage collected, it's value will also be collected!
No memory leaks.
const EnginePart = (() => {
// Store `this` in the WeakMap so that it's weakly referenced
const _privateStore = new WeakMap();
// Function to store the reference key, and get its value
function storage (object) {
if (!_privateStore.has(object))
_privateStore.set(object, Object.create(null));
return _privateStore.get(object);
};
const Part = Object.create(null, {
init: {
value: function () {
storage(this).part_number = "4104928345103";
storage(this).weight_in_kg = 1.7;
return this;
}
},
getPartNumber: {
value: function () {
return storage(this).part_number;
}
}
});
return Part.init();
})();
Put it in the Old Tool Belt
Since I plan on using this mechanism in several places in my application, I needed to abstract it away into a utility.
/*
gutil is the handy, global object I create in most apps
to hold utility functions
*/
let gutil = Object.create(null);
/*
The privy allows modules to have weakly-held private data contained
in a new WeakMap. It returns a function to access that WeakMap.
*/
gutil.privy = Object.create(null, {
init: {
value: function () {
const _private = new WeakMap();
return function (object) {
if (!_private.has(object))
_private.set(object, Object.create(null));
return _private.get(object);
};
}
}
});
Then I can use it inside an IIFE that builds a module of my application.
"use strict";
var Gauntlet = function (global) {
const storage = gutil.privy.init(); // Private store
// The Horde will contain all monster combatants
const Horde = Object.create(null);
// Initialization
Object.defineProperty(Horde, "init", {
value: function () {
storage(this).horde = new WeakMap();
storage(this).names = new Set();
return this;
},
enumerable: true
});
// Return all monsters
Object.defineProperty(Horde, "all", {
value: function () {
return storage(this).horde;
},
enumerable: true
});
// Get a specific type of monster
Object.defineProperty(Horde, "soldier", {
value: function () {
return Object.create(storage(this).horde.get(type));
},
enumerable: true
});
global.Horde = Horde.init();
return global;
}(Gauntlet || {});