This article is a section from the course ES6 in Practice. I created this course during the last couple of months, because there is an evident need for a resource that helps JavaScript developers put theory into practice.
This course won't waste your time with long hours of video, or long pages of theory that you will never use in practice. We will instead focus on learning just enough theory to solve some exercises. Once you are done with the exercises, you can check the reference solutions to deepen your understanding. In fact, you will sometimes get a chance to discover some theoretical concepts by solving an exercise, then reading about the concept in the reference solution.
If you like this article, check out the course here.
Symbols: a new ES6 primitive type and its use cases
ES6 introduces a new primitive type for JavaScript: Symbols. A JavaScript symbol is created by the global Symbol()
function. Each time the Symbol()
function is called, a new unique symbol is returned.
1 2 3 4 5 6 7 |
let symbol1 = Symbol(); let symbol2 = Symbol(); console.log( symbol1 === symbol2 ); > false |
Symbols don't have a literal value. All you should know about the value of a symbol is that each symbol is treated as a unique value. In other words, no two symbols are equal.
Symbol is a new type in JavaScript.
1 2 3 4 |
console.log( typeof symbol1 ); > "symbol" |
Symbols are useful, because they act as unique object keys.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
let myObject = { publicProperty: 'Value of myObject[ "publicProperty" ]' }; myObject[ symbol1 ] = 'Value of myObject[ symbol1 ]'; myObject[ symbol2 ] = 'value of myObject[ symbol2 ]'; console.log( myObject ); > Object > publicProperty: "Value of myObject[ "publicProperty" ]" > Symbol(): "Value of myObject[ symbol1 ]" > Symbol(): "value of myObject[ symbol2 ]" > __proto__: Object console.log( myObject[ symbol1 ] ); > Value of myObject[ symbol1 ] |
When console logging myObject
, you can see that both symbol properties are stored in the object. The literal "Symbol()"
is the return value of the toString()
method called on the symbol. This value denotes the presence of a symbol key in the console. We can retrieve the corresponding values if we have access to the right symbol.
Properties with a symbol key don't appear in the JSON representation of your object. Not even the for-in loop or Object.keys
can enumerate them:
1 2 3 4 5 6 7 8 9 10 11 12 |
JSON.stringify( myObject ) > "{"publicProperty":"Value of myObject[ \"publicProperty\" ] "}" for( var prop in myObject ) { console.log( prop, myObject[prop] ); } > publicProperty Value of myObject[ "publicProperty" ] console.log( Object.keys( myObject ) ); > ["publicProperty"] |
Even though properties with Symbol keys don't appear in the above cases, these properties are not fully private in a strict sense. Object.getOwnPropertySymbols
provides a way to retrieve the symbol keys of your objects:
1 2 3 4 5 6 7 |
Object.getOwnPropertySymbols(myObject) > [Symbol(), Symbol()] myObject[ Object.getOwnPropertySymbols(myObject)[0] ] > "Value of myObject[ symbol1 ]" |
If you choose to represent private variables with Symbol keys, make sure you don't use
Object.getOwnPropertySymbols
to retrieve properties that are intended to be private. In this case, the only use cases forObject.getOwnPropertySymbols
are testing and debugging.
As long as you respect the above rule, your object keys will be private from the perspective of developing your code. In practice however, be aware that others will be able to access your private values.
Even though symbol keys are not enumerated by for...of
, the spread operator, or Object.keys
, they still make it to shallow copies of our objects:
1 2 3 4 5 6 7 8 9 10 |
clonedObject = Object.assign( {}, myObject ); console.log( clonedObject ); > Object > publicProperty: "Value of myObject[ "publicProperty" ]" > Symbol(): "Value of myObject[ symbol1 ]" > Symbol(): "value of myObject[ symbol2 ]" > __proto__: Object |
Naming your symbols properly is essential in indicating what your symbol is used for. If you need additional semantic guidance, it is also possible to attach a description to your symbol. The description of the symbol appears in the string value of the symbol.
1 2 3 4 5 6 7 |
let leftNode = Symbol( 'Binary tree node' ); let rightNode = Symbol( 'Binary tree node' ); console.log( leftNode ) > Symbol(Binary tree node) |
Always provide a description for your symbols, and make your descriptions unique. If you use symbols for accessing private properties, treat their descriptions as if they were variable names.
Even if you pass the same description to two symbols, their value will still differ. Knowing the description does not make it possible for you to create the same symbol.
1 2 3 4 |
console.log( leftNode === rightNode ); > false |
Global symbol registry
ES6 has a global resource for creating symbols: the symbol registry. The symbol registry provides us with a one-to-one relationship between strings and symbols. The registry returns symbols using Symbol.for( key )
.
Symbol.for( key1 ) === Symbol.for( key2 )
whenever key1 === key2
. This correspondance works even across service workers and iframes.
1 2 3 4 5 6 7 8 9 10 |
let privateProperty1 = Symbol.for( 'firstName' ); let privateProperty2 = Symbol.for( 'firstName' ); myObject[ privateProperty1 ] = 'Dave'; myObject[ privateProperty2 ] = 'Zsolt'; console.log( myObject[ privateProperty1 ] ); // Zsolt |
As there is a one-to-one correspondence between symbol values and their string keys in the symbol registry, it is also possible to retrieve the string key. Use the Symbol.keyFor
method.
1 2 3 4 5 6 7 |
Symbol.keyFor( privateProperty1 ); > "firstName" Symbol.keyFor( Symbol() ); > undefined |
Symbols as semi-private property keys
Creating truly private properties and operations is feasible, but it's not an obvious task in JavaScript. If it was as obvious as in Java, blog posts like this, this, this, this, and many more wouldn't have emerged.
Check out Exercise 2 at the bottom of this article to find out more about how to simulate private variables in JavaScript to decide whether it's worth for you.
Even though Symbols do not make attributes private, they can be used as a notation for private properties. You can use symbols to separate the enumeration of public and private properties, and the notation also makes it clear.
1 2 3 4 5 6 7 8 9 10 11 |
const _width = Symbol('width'); class Square { constructor( width0 ) { this[_width] = width0; } getWidth() { return this[_width]; } } |
As long as you can hide the _width
constant, you should be fine. One option to hide _width
is to create a closure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
let Square = (function() { const _width = Symbol('width'); class Square { constructor( width0 ) { this[_width] = width0; } getWidth() { return this[_width]; } } return Square; } )(); |
The advantage of this approach is that it becomes intentionally harder to access the private _width
value of our objects. It is also evident which of our properties are intended to be public, an which are intended to be private. The solution is not bulletproof, but some developers do use this approach in favor of indicating privacy by starting a variable with underscore.
The drawbacks are also obvious:
- By calling
Object.getOwnPropertySymbols
, we can get access to the symbol keys. Therefore, private fields are not truly private - developer experience is also worse, as you have to write more code. Accessing private properties is not as convenient as in Java or TypeScript for example
Some developers will express their opinion on using symbols for indicating privacy. In practice, your team has the freedom of deciding which practices to stick to, and which rules to follow. If you agree on using symbols as private keys, it is a working solution, as long as you don't start writing workarounds to publicly access private field values.
If you use symbols to denote private fields, you have done your best to indicate that a property is not to be accessed publicly. When someone writes code violating this common sense intention, they should bear the consequences.
There are various methods for structuring your code such that you indicate that some of your variables are private in JavaScript. None of them looks as elegant as a private
access modifier.
If you want true privacy, you can achieve it even without using ES6. Exercise 2 deals with this topic. Try to solve it, or read the reference solution.
The question is not whether it is possible to simulate private fields in JavaScript. The real question is whether you want to simulate them or not. Once you figure out that you don't need truly private fields for development, you can agree whether you use symbols, weak maps (see later), closures, or a simple underscore prefix in front of your variables.
Creating enum types
Enums allow you to define constants with semantic names and unique values. Given that the values of symbols are different, they make excellent values for enumerated types.
1 2 3 4 5 6 7 8 |
const directions = { UP : Symbol( 'UP' ), DOWN : Symbol( 'DOWN' ), LEFT : Symbol( 'LEFT' ), RIGHT: Symbol( 'RIGHT' ) }; |
Avoiding name clashes
When using symbols as identifiers for objects, we don't have to set up a global registry of available identifiers. We also save creation of a new identifier, as all we need to do is create a Symbol()
.
Same holds for external libraries.
Well known symbols
There are some well known symbols defined to access and modify internal JavaScript behavior. You can do magic such as redefining built-in methods, operators, and loops.
It is cool to apply hacks to the language, but ask yourself, is this skill going to move you forward in your career?
We will not focus on well known symbols in this section. If there is a valid use case for it, I will signal it in the corresponding lesson. Otherwise, I suggest staying away from manipulating the expected behavior of your code.
Exercises
Exercise 1. What are the pros and cons of using an underscore prefix for expressing our intention that a field is private? Compare this approach with symbols!
1 2 3 4 5 6 |
let mySquare { _width: 5, getWidth() { return _width; } } |
Solution:
Pros:
- notation and developer experience is simple, provided that your team spreads this practice
- it does not result in a hard-to-read code structure, all you need is one more character
Cons:
- the properties are not private in practice, they are just denoted as private, which opens up a possibility of hacking quick and dirty solutions
- unlike symbols, there is no clear separation between public and private properties. Private properties appear in the public interface of an object and they are enumerated in
for..of
loops, using the spread operator, andObject.keys
Exercise 2. Find a way to simulate truly private fields in JavaScript!
Solution:
When it comes to constructor functions, private members can be declared inside a constructor function using var
, let
, or const
.
1 2 3 4 5 6 7 8 9 10 11 |
function F() { let privateProperty = 'b'; this.publicProperty = 'a'; } let f = new F(); // f.publicProperty returns 'a' // f.privateProperty returns undefined |
In order to use the same idea for classes, we have to place the method definitions that use private properties in the constructor method in a scope where the private properties are accessible. We will use Object.assign
to accomplish this goal. This solution was inspired by an article I read on this topic by Dr. Axel Rauschmayer on Managing private data of ES6 classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class C { constructor() { let privateProperty = 'a'; Object.assign( this, { logPrivateProperty() { console.log( privateProperty ); } } ); } } let c = new C(); c.logPrivateProperty(); |
The field privateProperty
is not accessible in the c
object.
The solution also works when we extend the C
class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class D extends C { constructor() { super(); console.log( 'Constructor of D' ); } } let d = new D() > Constructor of D d.logPrivateProperty() > a |
For the sake of completeness, there are two other ways for creating private variables:
- Weak maps: we will introduce it in a later section. We can achieve true privacy with it, at the expense of writing less elegant code,
- TypeScript: introduces compile time checks whether our code treats private variables as private.
News on ES6 in Practice
I have decided on moving the course to LeanPub. For simplicity, you can download the book and the workbook as one file, which is a change compared to the original package.
I will continuously update the Leanpub package. If you purchased the course from another platform, before releasing the next update, I will get in touch with you with a dedicated 100% off survey.
Talking about updates, expect an update on December 30th. There will also be a price increase on January 1st. The update on December 30th contains the following changes:
- A revision of all existing chapters, exercises, and solutions
- the Reflect API
- Proxies
- Extensions of the
Math
andNumber
objects - At least ten new exercises and solutions
Following the philosophy behind Leanpub, by purchasing ES6 in Practice, you will get all future updates free of charge.
