ponyfoo.com

ES6 Symbols in Depth

Buon giorno! Willkommen to ES6 – “I can’t believe this is yet another installment” – in Depth. If you have no idea how you got here or what ES6 even is, I recommend reading A Brief History of ES6 Tooling. Then, make your way through destructuring, template literals, arrow functions, the spread operator and rest parameters, improvements coming to object literals, the new classes sugar on top of prototypes, let, const, and the “Temporal Dead Zone”, iterators, and generators. Today we’ll be discussing Symbols.

Improve this article
Nicolás Bevacqua
| 13 minute read | 10

Like I did in previous articles on the series, I would love to point out that you should probably set up Babel and follow along the examples with either a REPL or the babel-node CLI and a file. That’ll make it so much easier for you to internalize the concepts discussed in the series. If you aren’t the “install things on my computer” kind of human, you might prefer to hop on CodePen and then click on the gear icon for JavaScript – they have a Babel preprocessor which makes trying out ES6 a breeze. Another alternative that’s also quite useful is to use Babel’s online REPL – it’ll show you compiled ES5 code to the right of your ES6 code for quick comparison.

Before getting into it, let me shamelessly ask for your support if you’re enjoying my ES6 in Depth series. Your contributions will go towards helping me keep up with the schedule, server bills, keeping me fed, and maintaining Pony Foo as a veritable source of JavaScript goodies.

Thanks for listening to that, and let’s go into symbols now! For a bit of context, you may want to check out the last two articles, – on iterators and generators – where we first talked about Symbols.

What are Symbols?

Symbols are a new primitive type in ES6. If you ask me, they’re an awful lot like strings. Just like with numbers and strings, symbols also come with their accompanying Symbol wrapper object.

We can create our own Symbols.

var mystery = Symbol()

Note that there was no new. The new operator even throws a TypeError when we try it on Symbol.

var oops = new Symbol()
// <- TypeError

For debugging purposes, you can describe symbols.

var mystery = Symbol('this is a descriptive description')

Symbols are immutable. Just like numbers or strings. Note however that symbols are unique, unlike primitive numbers and strings.

console.log(Symbol() === Symbol())
// <- false
console.log(Symbol('foo') === Symbol('foo'))
// <- false

Symbols are symbols.

console.log(typeof Symbol())
// <- 'symbol'
console.log(typeof Symbol('foo'))
// <- 'symbol'

There are three different flavors of symbols – each flavor is accessed in a different way. We’ll explore each of these and slowly figure out what all of this means.

  • You can access local symbols by obtaining a reference to them directly
  • You can place symbols on the global registry and access them across realms
  • “Well-known” symbols exist across realms – but you can’t create them and they’re not on the global registry

What the heck is a realm, you say? A realm is spec-speak for any execution context, such as the page your application is running in, or an <iframe> within your page.

The “Runtime-Wide” Symbol Registry

There’s two methods you can use to add symbols to the runtime-wide symbol registry: Symbol.for(key) and Symbol.keyFor(symbol). What do these do?

Symbol.for(key)

This method looks up key in the runtime-wide symbol registry. If a symbol with that key exists in the global registry, that symbol is returned. If no symbol with that key is found in the registry, one is created. That’s to say, Symbol.for(key) is idempotent. In the snippet below, the first call to Symbol.for('foo') creates a symbol, adds it to the registry, and returns it. The second call returns that same symbol because the key is already in the registry by then – and associated to the symbol returned by the first call.

Symbol.for('foo') === Symbol.for('foo')
// <- true

That is in contrast to what we knew about symbols being unique. The global symbol registry however keeps track of symbols by a key. Note that your key will also be used as a description when the symbols that go into the registry are created. Also note that these symbols are as global as globals get in JavaScript, so play nice and use a prefix and don’t just name your symbols 'user' or some generic name like that.

Symbol.keyFor(symbol)

Given a symbol symbol, Symbol.keyFor(symbol) returns the key that was associated with symbol when the symbol was added to the global registry.

var symbol = Symbol.for('foo')
console.log(Symbol.keyFor(symbol))
// <- 'foo'

How Wide is Runtime-Wide?

Runtime-wide means the symbols in the global registry are accessible across code realms. I’ll probably have more success explaining this with a piece of code. It just means the registry is shared across realms.

var frame = document.createElement('iframe')
document.body.appendChild(frame)
console.log(Symbol.for('foo') === frame.contentWindow.Symbol.for('foo'))
// <- true

The “Well-Known” Symbols

Let me put you at ease: these aren’t actually well-known at all. Far from it. I didn’t have any idea these things existed until a few months ago. Why are they “well-known”, then? That’s because they are JavaScript built-ins, and they are used to control parts of the language. They weren’t exposed to user code before ES6, but now you can fiddle with them.

A great example of a “well-known” symbol is something we’ve already been playing with on Pony Foo: the Symbol.iterator well-known symbol. We used that symbol to define the @@iterator method on objects that adhere to the iterator protocol. There’s a list of well-known symbols on MDN, but few of them are documented at the time of this writing.

One of the well-known symbols that is documented at this time is Symbol.match. According to MDN, you can set the Symbol.match property on regular expressions to false and have them behave as string literals when matching (instead of regular expressions, which don’t play nice with .startsWith, .endsWith, or .includes).

This part of the spec hasn’t been implemented in Babel yet, – I assume that’s just because it’s not worth the trouble – but supposedly it goes like this.

var text = '/foo/'
var literal = /foo/
literal[Symbol.match] = false
console.log(text.startsWith(literal))
// <- true

Why you’d want to do that instead of just casting literal to a string is beyond me.

var text = '/foo/'
var casted = /foo/.toString()
console.log(text.startsWith(casted))
// <- true

I suspect the language has legitimate performance reasons that warrant the existence of this symbol, but I don’t think it’ll become a front-end development staple anytime soon.

Regardless, Symbol.iterator is actually very useful, and I’m sure other well-known symbols are useful as well.

Note that well-known symbols are unique, but shared across realms, even when they’re not accessible through the global registry.

var frame = document.createElement('iframe')
document.body.appendChild(frame)
console.log(Symbol.iterator === frame.contentWindow.Symbol.iterator)
// <- true

Not accessible through the global registry? Nope!

console.log(Symbol.keyFor(Symbol.iterator))
// <- undefined

Accessing them statically from anywhere should be more than enough, though.

Symbols and Iteration

Any consumer of the iterable protocol obviously ignores symbols other than the well-known Symbol.iterator that would define how to iterate and help identify the object as an iterable.

var foo = {
  [Symbol()]: 'foo',
  [Symbol('foo')]: 'bar',
  [Symbol.for('bar')]: 'baz',
  what: 'ever'
}
console.log([...foo])
// <- []

The ES5 Object.keys method ignores symbols.

console.log(Object.keys(foo))
// <- ['what']

Same goes for JSON.stringify.

console.log(JSON.stringify(foo))
// <- {"what":"ever"}

So, for..in then? Nope.

for (let key in foo) {
  console.log(key)
  // <- 'what'
}

I know, Object.getOwnPropertyNames. Nah! – but close.

console.log(Object.getOwnPropertyNames(foo))
// <- ['what']

You need to be explicitly looking for symbols to stumble upon them. They’re like JavaScript neutrinos. You can use Object.getOwnPropertySymbols to detect them.

console.log(Object.getOwnPropertySymbols(foo))
// <- [Symbol(), Symbol('foo'), Symbol.for('bar')]

The magical drapes of symbols drop, and you can now iterate over the symbols with a for..of loop to finally figure out the treasures they were guarding. Hopefully, they won’t be as disappointing as the flukes in the snippet below.

for (let symbol of Object.getOwnPropertySymbols(foo)) {
  console.log(foo[symbol])
  // <- 'foo'
  // <- 'bar'
  // <- 'baz'
}

Why Would I Want Symbols?

There’s a few different uses for symbols.

Name Clashes

You can use symbols to avoid name clashes in property keys. This is important when following the “objects as hash maps” pattern, which regularly ends up failing miserably as native methods and properties are overridden unintentionally (or maliciously).

“Privacy”?

Symbols are invisible to all “reflection” methods before ES6. This can be useful in some scenarios, but they’re not private by any stretch of imagination, as we’ve just demonstrated with the Object.getOwnPropertySymbols API.

That being said, the fact that you have to actively look for symbols to find them means they’re useful in situations where you want to define metadata that shouldn’t be part of iterable sequences for arrays or any iterable objects.

Defining Protocols

I think the biggest use case for symbols is exactly what the ES6 implementers use them for: defining protocols – just like there’s Symbol.iterator which allows you to define how an object can be iterated.

Imagine for instance a library like dragula defining a protocol through Symbol.for('dragula.moves'), where you could add a method on that Symbol to any DOM elements. If a DOM element follows the protocol, then dragula could call the el[Symbol.for('dragula.moves')]() user-defined method to assert whether the element can be moved.

This way, the logic about elements being draggable by dragula is shifted from a single place for the entire drake (the options for an instance of dragula), to each individual DOM element. That’d make it easier to deal with complex interactions in larger implementations, as the logic would be delegated to individual DOM nodes instead of being centralized in a single options.moves method.

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments (10)

Marc-André wrote

Thanks for the article. I must admit I don’t understand your example as for defining protocol.

More specifically, I don’t see how a protocol asking to define a method Symbol.for('dragula.moves') would be an improvement over asking for a method 'dragula.moves' or dragula_moves. More keystrokes, no less risk of collision, not clearer.

Would you care to elaborate?

PS: I even thought one shouldn’t modify host objects, but that’s a bit tangential.

bcherny wrote

I was going to ask the same thing - how is el[Symbol.for('dragula.moves')] different from el.dragula.moves, el.getAttribute('dragula.moves'), or the centralized approach you’re saying this will replace? ie.

let instances = new WeakMap()
instances[el] = new Dragula()

Clearly not polluting the el object in an obvious way is nice, but a centralized approach still trumps polluting el at all right?

Nicolas Bevacqua wrote

Using a WeakMap is definitely an option here, but we still haven’t covered those in the series – they’re coming on friday, though.

WeakMap involves a bit more maintenance work and using Symbols is just easier. Pollution is a concern whenever you’re adding properties to an object, but not so much with symbols as those are mostly unique and really hard to stumble upon by accident.

bcherny wrote

Now that I think about it, we can do the same thing in ES5 (sans symbols), and avoid a centralized store’s GC issues without WeakMaps:

Object.defineProperty(el, 'dragulaMoves', {
  configurable: false,
  enumerable: false,
  value: 'static',
  writable: false
})
Charbz wrote

Thanks for the great article. There is one syntax error in the examples given above in the ‘what are symbols’ section :

console.log(Symbol(‘foo’) === Symbol('foo))

should be :

console.log(Symbol(‘foo’) === Symbol(‘foo’))

Cheers :)

Nicolas Bevacqua wrote

This was already fixed, thanks.

zloirock wrote

This part of the spec hasn’t been implemented in Babel yet

Babel does not implement symbols and other standard library features, babel uses core-js.

Strange case for demonstration well-known symbols. core-js adds support main use case of Symbol.match, Symbol.replace, Symbol.search, Symbol.split, core-js adds support Symbol.toStringTag and, of course, Symbol.iterator.

Well-known and global symbol registry can’t be polyfilled cross-realm and Symbol.iterator === frame.contentWindow.Symbol.iterator only with native implementation.