ES6 includes a new feature called Symbols that allow you to do a number of very interesting things (if you haven’t heard about symbols yet, check out the ExploringJS chapter and my own WatchMeCode videos on the ES6/Symbols). In my exploration of ES6 Classes, I found a very interesting use case for Symbols… one that applies to not only Classes, but any object where you want a named property to have a hidden or private backing variable.
The result is a more flexible way to hide the backing variable for an object’s properties, while still providing clean use of prototypes and allowing direct access to the backing variable when needed.
Getter / Setter Property Syntax
it’s very difficult to hide the backing variable.
The Problem: Hiding The Backing Variable
In the above example, the code is using “this._foo” to store and retrieve the value. This works great. You can add all kinds of code to the getter / setter and do some really advanced things with it.
However, this code is rather insecure in that anyone can access the “_foo” attribute where the data is stored.
In this example usage, the “foo” property is used via the setter. The value of “foo” is then retrieved from the getter and logged to the console.
On the next line, though, the backing variable “this._foo” is modified directly. Logging the value of “foo” to the console again, shows that the value has been modified.
This is not a good situation. Anyone that knows the name of the backing variable can easily access and modify it.
Hiding Via Closures
It is possible to avoid this situation using closures. You can have an object defined and it’s backing store of variables defined within a function, for example:
This works, and it’s a common pattern of hiding the backing variable.
You can see that the output of the second console.log is still the original “bar” value, because setting “obj._foo” does nothing to the backing “_foo” variable.
However, this code quickly gets turns ugly and difficult when you’re dealing with constructor functions and prototypes.
Things Don’t Always Play Well
Take, for example, a simple modification to the above closure based attribute hiding. If this is turned into a constructor function with a prototype method attached, the code will fail:
The call to “obj.doBar()” fails because the “obj” object is not inheriting from the prototype. Fixing that requires additional code in the constructor function, including a change in how the getter / setter are defined.
Fixing The Prototype And Closure
To fix this, the object literal needs to be removed and the other form of getter / setter creation needs to be used:
Here, the property is created using Object.defineProperty instead of the object literal syntax. This works well with the prototype and the closure around the backing “_foo” variable.
The result is fairly clean, and a common pattern for hiding backing variables with ES5 objects. It does, however, produce objects that do not share the definition of the “foo” getter / setter.
Each object instance receives it’s own definition of the “foo” property. Normally, this isn’t a problem. In cases where you are creating a very large number of these objects, it can cause some memory bloat.
Hiding The Backing Variable With ES6 Symbols
With the advent of ES6 Symbols, you have another option available for this situation – hiding the backing variable on the object instance, using Symbols.
In this example, the property definition is moved to the object prototype, instead of defined within the constructor function. This provides a little better memory use, as it shares the object definition between instances.
There is also a new “Symbol” being created and used as the backing store. The “_fooAttr” variable is assigned to a Symbol with a descriptor of “foo”. Within the property getter / setter, this symbol is used as the attribute identifier on the object instance.
Now, when you use this code from another file (a CommonJS / Browserify module in this case), the “foo” property can be accessed as expected. However, it isn’t possible to directly modify the underlying value store from outside of the code that defines the original Symbol:
No matter how many times I call Symbol(“foo”), I will never receive the same Symbol instance. Therefore, I will never be able to access the value stored with the original Symbol as the key, from outside of the code that defines it (not in this example, anyways – there is a way to do it, shown below).
The end result, then, is an object that has a getter / setter property using a private variable as the backing store for that property. It also takes advantage of prototypes to conserve memory usage and maintain the prototype chain of shared code.
Direct Modification via Symbols
With the closure version, you cannot access the backing variable except from where it was defined. With Symbols, you can access the backing variable, but it requires hoops to jump through and deliberate code for this to happen.
In this code, the call to Object.getOwnPropertySymbols returns an array of Symbols that are stored on the object. You can grab the appropriate one (by index only) and modify the underlying data, directly. However, this makes it less likely that someone will accidentally abuse your objects’ underlying variables, because there are hoops to jump through and you can only access the Symbol via index from the array.
Benefits, Drawbacks, And Options
For example, with the Symbol, you can get access to the underlying variable using Object.getOwnPropertySymbols, as shown above. This method would allow you to retrieve the symbols that are used to store data on the object, and then modify the underlying data for an object. While this may seem like a bad thing, at first, it does allow some additional flexibility and capabilities but requires additional hoops to jump through to make it happen.
Whether or not this feature and syntax use is beneficial to you is largely based on your needs and opinions of how the code should be written. The “fixed” version of the closure and prototype is a common and valuable pattern, and the ES6 Symbol version is only a new option – not a golden hammer that should always be used.