Using symbols to create (sort of) private properties

Using symbols to create (sort of) private properties

20 Aug 2020 symbols javascript

Languages like PHP and many others allow you to specify class properties/methods as private (or protected, in the case of PHP). In JavaScript, however, class properties and methods are all public by default.

There is agreed syntax for this in JavaScript, but the feature is still experimental, and so not usable as yet. All the same, it looks like this:

class Foo() { #thisIsPrivate = 1; //static, and can't be called from outside the class declaration #privateMethod() null //ditto }

So until then? Well, it turns out symbols can be used to (sort) of keep properties private.

What are symbols? 🔗

Feel free to skip this part if you already know about symbols.

Yup. Symbols arrived in JavaScript in ECMAScript 2015, and represented a new kind of primitive, alongside strings, booleans and others. They look like this:

let mySymbol = Symbol('some description');

Symbols cannot be constructed; doing new Symbol('foo') will throw an error.

The key thing about symbols is no two are ever identical - even if we give them the same description.

Symbol('a') === Symbol('a'); //false

The description, incidentally, is completely optional. It's just a way to help identify a symbol later, if need be, via the description property.

let mySymbol = Symbol('monster'); mySymbol.description; //"monster"

They're intended as object properties, like so:

let sym1 = Symbol(), sym2 = Symbol(), myObj = { foo: 0, [sym1]: 'private!', [sym2]: () => alert('Private method!') };

Crucially, though, properties and methods declared as symbols are not enumerable. And this is the key to using them as privates.

Symbols as privates 🔗

Let's play with the object we just made. In each case you'll see that we don't get access to the properties declared as symbols, owing to their non-enumerability.

for (let i in myObj) console.log(i); //"foo" only - not the other two properties

Dang. Let's try some other way.

Object.values(myObj); //[0] - again, only the first value

The symbol-created properties are accessible only with access to the symbol itself.

myObj[sym1]; //"private!"

Without that, the properties are inaccessible. From here, it should be pretty clear where this is heading now. Let's look at a fuller example.

const Person = (() => { let creditRatingSymbol = Symbol('credit rating'); return class { constructor(name, age) { this.name = name; this.age = age; this[creditRatingSymbol] = 'TERRIBLE! DO NOT LEND!'; } lendOrNot() { return !this[creditRatingSymbol].includes('TERRIBLE'); } } })(); bloke = new Person('Bob', 14); bloke.lendOrNot(); //false

OK so what's happening there? We have a class definition where the constructor sets some basic info on the instance but also some "private" info, in the form of a credit rating.

Because the credit rating is declared via a symbol, not a simple string-based property name, and the symbol is not accessible to the consumer code (that's the reason for our outer immediately invoked function expression, or IIFE), it is not accessible or deletable.

It will show up if the user console.log()'s the instance, but will remain otherwise private.

"Sort of" private? 🔗

Right up top I said symbols could sort of keep properties private. There is one way the consumer can get hold symbol-declared properties, and that's via Object.getOwnPropertySymbol() method.

let obj = { a: 1, b: 2, [Symbol('c')]: 3 }; let syms = Object.getOwnPropertySymbols(obj); //[<Symbol>]

Armed with that, we can get the secret juicy data (er, our 3).

obj[syms[0]]; //3

But that's a world away from mindless overwrites/deletions - anyone doing that really wants to access that symbol. And if they're going to put that much effort in, perhaps they deserve it. Otherwise, we just have to wait for the new syntax to gain browser support.

Other approaches 🔗

There are other ways to handle private data. One of the older ways is not to make your private data properties at all, and just reference them from constructor args via getters.

function Task(name) { return { getName() { return name; } }; } let task = new Task('mow lawn'); task.name = 'phone dad'; task.getName(); //"mow lawn" - not been changed

Because the task name was never declared as a property, and is instead accessible only via a getter function, we can refer to a private bit of data - the original constructor argument - internally, rather than exposing something that can be deleted or modified.

Yet another way involves exploiting JavaScript's Weak Map API. I won't go into the details here, because the anonymous author of Curiosity Driven has already written a useful blog post looking at this.

In short, the technique rests on Weak Maps' use of objects as object keys, which are not enumerable (just like symbols).