JavaScript introduced symbols in ES6 as a way to prevent property name collisions. As an added bonus, symbols also provide a way to simulate private properties in 2015-2019 JavaScript.

Introduction

The simplest way to create a symbol in JavaScript is to call the Symbol() function. The 2 key properties that makes symbols so special are:

Symbols can be used as object keys. Only strings and symbols can be used as object keys. No two symbols are ever equal.

const symbol1 = Symbol (); const symbol2 = Symbol (); symbol1 === symbol2; const obj = {}; obj[symbol1] = 'Hello' ; obj[symbol2] = 'World' ; obj[symbol1]; obj[symbol2];

Although the Symbol() call makes it look like symbols are objects, symbols are actually a primitive type in JavaScript. Using Symbol as a constructor with new throws an error.

const symbol1 = Symbol (); typeof symbol1; symbol1 instanceof Object ; new Symbol ();

Descriptions

The Symbol() function takes a single parameter, the string description . The symbol's description is only for debugging purposes - the description shows up in the symbol's toString() . However, two symbols with the same description are not equal.

const symbol1 = Symbol ( 'my symbol' ); const symbol2 = Symbol ( 'my symbol' ); symbol1 === symbol2; console .log(symbol1);

There is also a global symbol registry. Creating a symbol using Symbol.for() adds a symbol to a global registry, keyed by the symbol's description . In other words, if you create two symbols with the same description using Symbol.for() , the two symbols will be equal.

const symbol1 = Symbol .for( 'test' ); const symbol2 = Symbol .for( 'test' ); symbol1 === symbol2; console .log(symbol1);

Generally speaking, you shouldn't use the global symbol registry unless you have a very good reason to, because you might run into naming collisions.

Name Collisions

The first built-in symbol in JavaScript was the Symbol.iterator symbol in ES6. An object that has a Symbol.iterator function is considered an iterable. That means you can use that object as the right hand side of a for/of loop.

const fibonacci = { [ Symbol .iterator]: function *( ) { let a = 1 ; let b = 1 ; let temp; yield b; while ( true ) { temp = a; a = a + b; b = temp; yield b; } } }; for ( const x of fibonacci) { if (x >= 100 ) { break ; } console .log(x); }

Why is Symbol.iterator a symbol rather than a string? Suppose instead of using Symbol.iterator , the iterable spec checked for the presence of a string property 'iterator' . Furthermore, suppose you had the below class that was meant to be an iterable.

class MyClass { constructor (obj) { Object .assign( this , obj); } iterator() { const keys = Object .keys( this ); let i = 0 ; return ( function *( ) { if (i >= keys.length) { return ; } yield keys[i++]; })(); } }

Instances of MyClass will be iterables that allow you to iterate over the object's keys. But the above class also has a potential flaw. Suppose a malicious user were to pass an object with an iterator property to MyClass .

const obj = new MyClass({ iterator: 'not a function' });

If you were to use for/of with obj , JavaScript would throw TypeError: obj is not iterable . That's because the user-specified iterator function would overwrite the class' iterator property. This is a similar security issue to prototype pollution, where naively copying user data may cause issues with special properties like __proto__ and constructor .

The key pattern here is that symbols enable a clear separation between user data and program data in objects. Since symbols cannot be represented in JSON, there's no risk of data passed into an Express API having a bad Symbol.iterator property. In objects that mix user data with built-in functions and methods, like Mongoose models, you can use symbols to ensure that user data doesn't conflict with your built-in functionality.

Private Properties

Since no two symbols are ever equal, symbols are a convenient way to simulate private properties in JavaScript. Symbols don't show up in Object.keys() , and therefore, unless you explicitly export a symbol, no other code can access that property unless you explicitly go through the Object.getOwnPropertySymbols() function.

function getObj ( ) { const symbol = Symbol ( 'test' ); const obj = {}; obj[symbol] = 'test' ; return obj; } const obj = getObj(); Object .keys(obj); obj[ Symbol ( 'test' )]; const [symbol] = Object .getOwnPropertySymbols(obj); obj[symbol];

Symbols are also convenient for private properties because they do not show up in JSON.stringify() output. Specifically, JSON.stringify() silently ignores symbol keys and values.

const symbol = Symbol ( 'test' ); const obj = { [symbol]: 'test' , test: symbol }; JSON .stringify(obj);

Moving On