ES6 in Action: Symbols and Their Uses

While ES2015 has introduced many language features that have been on developers’ wish lists for some time, there are some new features that are less well known and understood, and the benefits of which are much less clear — such as symbols.

The symbol is a new primitive type, a unique token that’s guaranteed never to clash with another symbol. In this sense, you could think of symbols as a kind of UUID (universally unique identifier). Let’s look at how symbols work, and what we can do with them.

Creating New Symbols

Creating new symbols is very straightforward and is simply a case of calling the Symbol function. Note that this is a just a standard function and not an object constructor. Trying to call it with the new operator will result in a TypeError. Every time you call the Symbol function, you’ll get a new and completely unique value.

const foo = Symbol();
const bar = Symbol();

foo === bar
// <-- false
--ADVERTISEMENT--

Symbols can also be created with a label, by passing a string as the first argument. The label doesn’t affect the value of the symbol, but is useful for debugging, and is shown if the symbol’s toString() method is called. It’s possible to create multiple symbols with the same label, but there’s no advantage to doing so and this would probably just lead to confusion.

let foo = Symbol('baz');
let bar = Symbol('baz');

foo === bar
// <-- false
console.log(foo);
// <-- Symbol(baz)

What Can I Do With Symbols?

Symbols could be a good replacement for strings or integers as class/module constants:

class Application {
  constructor(mode) {
    switch (mode) {
      case Application.DEV:
        // Set up app for development environment
        break;
      case Application.PROD:
        // Set up app for production environment
        break;
      case default:
        throw new Error('Invalid application mode: ' + mode);
    }
  }
}

Application.DEV = Symbol('dev');
Application.PROD = Symbol('prod');

// Example use
const app = new Application(Application.DEV);

String and integers are not unique values; values such as the number 2 or the string development, for example, could also be in use elsewhere in the program for different purposes. Using symbols means we can be more confident about the value being supplied.

Another interesting use of symbols is as object property keys. If you’ve ever used a JavaScript object as a hashmap (an associative array in PHP terms, or dictionary in Python) you’ll be familiar with getting/setting properties using the bracket notation:

const data = [];

data['name'] = 'Ted Mosby';
data['nickname'] = 'Teddy Westside';
data['city'] = 'New York';

Using the bracket notation, we can also use a symbol as a property key. There are a couple of advantages to doing so. First, you can be sure that symbol-based keys will never clash, unlike string keys, which might conflict with keys for existing properties or methods of an object. Second, they won’t be enumerated in for … in loops, and are ignored by functions such as Object.keys(), Object.getOwnPropertyNames() and JSON.stringify(). This makes them ideal for properties that you don’t want to be included when serializing an object.

const user = {};
const email = Symbol();

user.name = 'Fred';
user.age = 30;
user[email] = 'fred@example.com';

Object.keys(user);
// <-- Array [ "name", "age" ]

Object.getOwnPropertyNames(user);
// <-- Array [ "name", "age" ]

JSON.stringify(user);
// <-- "{"name":"Fred","age":30}"

It’s worth noting, however, that using symbols as keys doesn’t guarantee privacy. There are some new tools provided to allow you to access symbol-based property keys. Object.getOwnPropertySymbols() returns an array of any symbol-based keys, while Reflect.ownKeys() will return an array of all keys, including symbols.

Object.getOwnPropertySymbols(user);
// <-- Array [ Symbol() ]

Reflect.ownKeys(user)
// <-- Array [ "name", "age", Symbol() ]

Well-known Symbols

Because symbol-keyed properties are effectively invisible to pre-ES6 code, they’re ideal for adding new functionality to JavaScript’s existing types without breaking backwards compatibility. The so-called “well-known” symbols are predefined properties of the Symbol function that are used to customize the behavior of certain language features, and are used to implement new functionality such as iterators.

Symbol.iterator is a well-known symbol that’s used to assign a special method to objects, which allows them to be iterated over:

const band = ['Freddy', 'Brian', 'John', 'Roger'];
const iterator = band[Symbol.iterator]();

iterator.next().value;
// <-- { value: "Freddy", done: false }
iterator.next().value;
// <-- { value: "Brian", done: false }
iterator.next().value;
// <-- { value: "John", done: false }
iterator.next().value;
// <-- { value: "Roger", done: false }
iterator.next().value;
// <-- { value: undefined, done: true }

The built-in types String, Array, TypedArray, Map and Set all have a default Symbol.iterator method which is called when an instance of one of these types is used in a for … of loop, or with the spread operator. Browsers are also starting to use the Symbol.iterator key to allow DOM structures such as NodeList and HTMLCollection to be iterated over in the same way.

The Global Registry

The specification also defines a runtime-wide symbol registry, which means that you can store and retrieve symbols across different execution contexts, such as between a document and an embedded iframe or service worker.

Symbol.for(key) retrieves the symbol for a given key from the registry. If a symbol doesn’t exist for the key, a new one is returned. As you might expect, subsequent calls for the same key will return the same symbol.

Symbol.keyFor(symbol) allows you to retrieve the key for a given symbol. Calling the method with a symbol that doesn’t exist in the registry returns undefined:

const debbie = Symbol.for('user');
const mike   = Symbol.for('user');

debbie === mike
// <-- true

Symbol.keyFor(debbie);
// <-- "user"

Use Cases

There are a couple of use cases where using symbols provides an advantage. One, which I touched on earlier in the article, is when you want to add “hidden” properties to objects that won’t be included when the object is serialized.

Library authors could also use symbols to safely augment client objects with properties or methods without having to worry about overwriting existing keys (or having their keys overwritten by other code). For example, widget components (such as date pickers) are often initialized with various options and state that needs to be stored somewhere. Assigning the widget instance to a property of the DOM element object is not ideal, because that property could potentially clash with another key. Using a symbol-based key neatly side steps this issue and ensures that your widget instance won’t be overwritten. See the Mozilla Hacks blog post ES6 in Depth: Symbols for a more detailed exploration of this idea.

Browser Support

If you want to experiment with symbols, mainstream browser support is quite good. As you can see, the current versions of Chrome, Firefox, Microsoft Edge and Opera support the Symbol type natively, along with Android 5.1 and iOS 9 on mobile devices. There are also polyfills available if you need to support older browsers.

Conclusion

Although the primary reason for the introduction of symbols seems to have been to facilitate adding new functionality to the language without breaking existing code, they do have some interesting uses. It’s worthwhile for all developers to have at least a basic knowledge of them, and be familiar with the most commonly used, well-known symbols and their purpose.