For a long time during my early career, I was an OO --- object oriented --- developer. I genuflected regularly in front of the altar of data encapsulation, object hierarchies and static typing. And the syntax. Oh the syntax!
But I have changed, of course, and so much of the dogma and ceremony that I participated in during those times has come to seem a lot less important than it was 20 years ago. Languages, and developers evolve. But that doesn't mean there aren't some really good lessons to learn.
Take, for instance, data encapsulation.
When I first began to seriously look at JavaScript as a language, data encapsulation - or the lack of it - was one of the things that really stuck in my old OO craw. While I loved the simplicity of the {}
data structure, I hated the fact that most properties I chose to add to it were typically just there - sticking out for everyone to see and perhaps corrupt. The language didn't make it very easy to keep this data protected. How do we handle this?
Take a look at how this simplistic approach to the {}
data structure might cause some real headaches. Here we have a productCatalog()
lookup function that returns a Product
data object. It might look something like this:
var product = productCatalog('widget-a');
console.log(product);
// Product { id: 2340847,
// name: 'widget-a',
// description: 'what a widget!',
// related: [Function] }
Notice that the object returned here contains a function, related()
which will find the set of products related to this one using this object's id
or name
property. But those properties are just there hanging on to the returned object by their fingernails. What if some evil bit of code came along and did this: product.id = 0x00034
just to see what would happen? How would the related()
function handle that? We just don't know.
There are ways to deal with this of course. One of the great things about JavaScript is how flexible it can be. Maybe the developer who wrote the productCatalog()
function knew some of these tricks. Here's one way to handle it using Javascript's Object.defineProperty
function.
function productCatalog( name ) {
if (findProduct(name)) {
return new Product(name);
}
return null;
}
function Product (name) {
this.name = name;
// lookup the product and populate
// this object's properties with appropriate values.
// Don't allow client code to modify our ID
Object.defineProperty(this, 'id', {
enumerable: false,
configurable: false,
writable: false,
value: 2340847
});
}
But... eeewwww.
Let's see how well that worked. At first things look great - no id
property on basic inspection. And if you do try to modify it, the value can't be changed. Yay!
console.log(productObject);
// Product { name: 'widget-a'
// description: 'what a widget!',
// related: [Function] }
productObject.id
// 2340847
productObject.id = 'foo'
productObject.id
// 2340847
But darn it. The property name appears in the Object.getOwnPropertyNames()
result. This isn't terrible, but we're not doing a great job of hiding data.
Object.getOwnPropertyNames(productObject)
// [ 'id', 'name', 'description', 'related' ]
What I'd really like is for the Product
object to have a reference to the id
but no way for client code to read it or even see it. Closures, for example, provide a way to do this. But that's really an entirely separate blog post, and what I really want to talk about here is ES6.
ECMAScript 2015
ES6 or ECMAScript 2015, as it is formally known, introduces lots of great new language features. I wish I had time to tell you about them all, but for now, I'll just focus on one subject. Data Hiding and Encapsulation.
There are a few new ways developers can approach this problem now, when using modern JavaScript interpreters with ES6 features available.
Getters
First let's take a look at Getters. ES6 getters allow you to easily use a function that makes a property read only. And since a getter is a function, the value could even be the result of some calculation. But that's not the point here.
Here's how you would use a getter in ES6 and how you could achieve the same functionality in ES5. The new syntax is way better.
// The ES6 way
let product = {
get id () { return 2340847; }
};
product.id
// 2340847
product.id = 'foo'
product.id
// 2340847
// The old way
var product = {};
Object.defineProperty(product, 'id', {
get: function() { return 2340847; },
enumerable: false,
configurable: false,
});
But this still doesn't really get what we want. There are two tools besides closures we can use to really and truly hide our data. Those are WeakMap
and Symbol
. Let's look at the WeakMap
first.
WeakMaps
The WeakMap
is a new data structure in ES6. It acts a lot like a regular map data structure. They are iterable
, and have getters and setters for objects. What makes them unique is that the keys are weakly referenced. This means, essentially, that when the only remaining reference to the key is the key itself, the entry is removed from the map. Here's how you can use the WeakMap
data structure to effectively hide private class data.
const privates = new WeakMap();
class Product {
constructor (name) {
this.name = name;
privates.set(this, {
id: 2340847
});
}
related () {
return lookupRelatedStuff( privates.get(this) );
}
}
Assuming this code is in a module that exports the productCatalog
function, there is no way for client code to see or modify the id
property. Success!
I like this approach. It's elegant and simple. The only real drawback I have found with this is performance. It's pretty expensive to do these WeakMap
lookups to get a handle on a property. So if performance is paramount. Consider using Symbol
as property keys.
Symbols
I have found that using properties whose keys are Symbol
s, while not as elegant as WeakMap
in my opinion is my preferred data hiding technique, because it's just so much faster.
One of the interesting things about Symbol
is that each Symbol
is unique. If we can keep the Symbol
private within our module,
then we don't have to worry about client code accessing the property. Here's how our Product
object would look if we took this approach.
const ID = Symbol('id');
class Product {
constructor (name) {
this.name = name;
this[ID] = 2340847;
}
related () {
return lookupRelatedStuff( this[ID] );
}
}
Additionally, when you use a Symbol
for a property key, the property does not appear in the list of properties returned from
Object.getOwnPropertyNames()
. This is nice. The downside is that the property leaks when using Reflect.ownKeys()
or Object.getOwnPropertySymbols()
.
const product = productCatalog('a-widget');
console.log(Reflect.ownKeys(product));
// [ 'name', Symbol(id) ]
console.log(Object.getOwnPropertySymbols(product));
// [ Symbol(id) ]
But I can live with that when performance matters. For Fidelity, we found that moving from WeakMap
to Symbol
for private data gave us a measurable, and quite significant performance boost. It's not ideal that the properties are visible. But since they are
inaccessible, I'll not worry about it too much.