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