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.

let symbol1 = Symbol(); let symbol2 = Symbol(); console.log( symbol1 === symbol2 ); > false 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.

console.log( typeof symbol1 ); > "symbol" 1 2 3 4 console . log ( typeof symbol1 ) ; > "symbol"

Symbols are useful, because they act as unique object keys.

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 ] 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:

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"] 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:

Object.getOwnPropertySymbols(myObject) > [Symbol(), Symbol()] myObject[ Object.getOwnPropertySymbols(myObject)[0] ] > "Value of myObject[ symbol1 ]" 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 for Object.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:

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 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.

let leftNode = Symbol( 'Binary tree node' ); let rightNode = Symbol( 'Binary tree node' ); console.log( leftNode ) > Symbol(Binary tree node) 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.

console.log( leftNode === rightNode ); > false 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.

let privateProperty1 = Symbol.for( 'firstName' ); let privateProperty2 = Symbol.for( 'firstName' ); myObject[ privateProperty1 ] = 'Dave'; myObject[ privateProperty2 ] = 'Zsolt'; console.log( myObject[ privateProperty1 ] ); // Zsolt 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.

Symbol.keyFor( privateProperty1 ); > "firstName" Symbol.keyFor( Symbol() ); > undefined 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.

const _width = Symbol('width'); class Square { constructor( width0 ) { this[_width] = width0; } getWidth() { return this[_width]; } } 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:

let Square = (function() { const _width = Symbol('width'); class Square { constructor( width0 ) { this[_width] = width0; } getWidth() { return this[_width]; } } return Square; } )(); 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

, 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.

const directions = { UP : Symbol( 'UP' ), DOWN : Symbol( 'DOWN' ), LEFT : Symbol( 'LEFT' ), RIGHT: Symbol( 'RIGHT' ) }; 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!

let mySquare { _width: 5, getWidth() { return _width; } } 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, and Object.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 .

function F() { let privateProperty = 'b'; this.publicProperty = 'a'; } let f = new F(); // f.publicProperty returns 'a' // f.privateProperty returns undefined 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.

class C { constructor() { let privateProperty = 'a'; Object.assign( this, { logPrivateProperty() { console.log( privateProperty ); } } ); } } let c = new C(); c.logPrivateProperty(); 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.

class D extends C { constructor() { super(); console.log( 'Constructor of D' ); } } let d = new D() > Constructor of D d.logPrivateProperty() > a 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 and Number objects

and 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.