I love writing in JavaScript. It's a flexible, dynamic language with a lot of nifty features and a great, evented runtime. JS gets a bad rep because of how often people use it poorly, and I think that's a very deserved reputation. Still, it's fun to work with and one of the first tools I reach for when fleshing out a quick proof-of-concept in code.
Working on larger codebases, however, means I want to have a prettier setup. I configure Babel so I can write the latest, greatest version of JS and have it automatically transpiled to work with older engines. I lint my code. I configure specific types for data encapsulation and force strict typing.
This all results in clean, maintainable, (usually) highly-documented libraries for my personal use and for work. However, there's one feature I'm used to in other languages that's missing in JS.
Privacy.
JavaScript Classes
ECMAScript 2015 (aka ES6) introduced classes to JavaScript. Older versions of the language would allow you to define an object type directly using function constructors, but didn't truly support object-oriented inheritance the way developers are used to.
Part of this is due to the fact that JavaScript is a prototypal language, not an object-oriented language. Or at least older versions weren't.
ES6 gave us classes with proper constructors, methods, properties, and inheritance. Instead of defining a rough object-like function in old-school JS:
function Rect(height, width) {
this.height = height;
this.width = width;
}
You could define a real class in ES6 and reuse it the same way you would a type in any other language:
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
The biggest unfortunate element of this class definition, though, is that there are no privacy modifiers. The constructor, methods, and properties of a JS class are all public by default. There's no nature of a protected method or a private property. At all.
A First Attempt
At one point in time, though, I discovered a quirky artifact of the way ES6 modules are defined. If it's not exposed directly, any "global" function or variable is really private to the module itself! The following code, for example, would expose a client instance for use elsewhere, but keep a database handle private.
let db = null;
class Client {
constructor(dbHandle) {
db = dbHandle
}
doQuery(query) {
db.exec(query)
}
}
The db variable inside this module is completely inaccessible to any code outside the module, yet it allows us to keep track of our database handle directly. This is exactly what we want, right?
Nope.
Since it's outside the class definition, this variable is treated as a static property. Yes, it's private to our module, but there's only one copy available. If we make multiple instances of our Client above, we'll overwrite db each time; the last instantiation will win and define the database handle used by all of the instances.
That's not what we want at all.
A Hacky Solution
While we don't necessarily want a shared database handle, the idea of a private, static property in our class definition does give us something to work with. Instead of defining our private properties directly within the module, we can define a container to house them and access it instead.
let container = {}
class Client {
constructor(dbHandle) {
this._id = Math.random().toString(36).substr(2, 9)
container[this._id] = {}
container[this._id].db = dbHandle
}
doQuery(query) {
let db = container[this._id].db
db.exec(query)
}
}
Now, the container is a shared object between instances of our class. Instances can then look up their own "private" properties housed within this container and use them during runtime.
The biggest drawbacks of this approach are:
- The syntax isn't the prettiest
- Every instance can, really, access the "private" data of any other instance of the class
- JavaScript doesn't feature the notion of class destructors, so you'll have to implement your own garbage collection routine to clean up and stored data when a class is destroyed
What other solutions have you implemented for class privacy in JavaScript?