JavaScript
Article

Preparing for ECMAScript 6: Symbols and Their Uses

By Nilson Jacques

This article was peer reviewed by Panayiotis «pvgr» Velisarakos, Simon Codrington, Tom Greco and Chris Perry. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

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. One such feature is the symbol. The symbol is a new primitive type, a unique token that is guaranteed never to clash with another symbol. In this sense, you could think of them as a kind of UUID (Universally Unique Identifier). Let’s look at how they work, and what we can do with them (Note: all examples in this article have been tested in the latest versions of Chrome and Firefox for Linux, but the behavior may vary from browser to browser or when using tools such as Firebug).

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 will get a new and completely unique value.

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

foo === bar
// <-- false

Symbols can also be created with a label, by passing a string as the first argument. The label does not 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 Them?

Symbols could often 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;
      default:
        throw new Error('Invalid application mode: ' + mode);
    }
  }
}

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

// Example use
let 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:

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

let user = {};
let 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 is worth noting, however, that using symbols as keys does not 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 are 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 which is used to assign a special method to objects which allows them to be iterated over.

let band = ['Freddy', 'Brian', 'John', 'Roger'];
let 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 does not 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 does not exist in the registry returns undefined.

let debbie = Symbol.for('user');
let 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 will not 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 intialized 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 will not 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 already quite good: https://kangax.github.io/compat-table/es6/. 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 is 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.

  • liammaddison

    Great read, thank you!

  • http://jeffreyleesmith.com/ Jeff Smith

    Excellent read. I’m liking dabbling in articles about ES6 and such recently. As you say, it’s good for developers to have some basic knowledge of these things, even if we don’t particularly use them day to day.
    Thanks for the write-up!

  • TroyR

    Great read, on a side note your first example `case default: throw new Error(‘Invalid application mode: ‘ + mode);` has a few issues; `case default` should just be `default` and you cannot convert a Symbol to string however you could setup the Symbols as `Symbol.for(‘dev’)` and then do the `Symbol.keyFor` to get the value in the Error.

    • Nilson Jacques

      Thanks, good catch with the default case, I’ve updated the article! Regarding the error message, I’d assumed that any non valid value was likely to be a string (or integer), but you’re right: if someone passed a non-valid Symbol you’d get a TypeError.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in JavaScript, once a week, for free.