What are symbols in JavaScript?

I stumbled across “symbols” when reading about JavaScript iterators. Here’s what I saw:

const myIterable = {};
myIterable[Symbol.iterator] = 
  () => { /* ... */ };

An iterable has the unusual property Symbol.iterator, but what on earth is Symbol.iterator? According to Node:

$ node
> Symbol.iterator
Symbol(Symbol.iterator)
> typeof Symbol.iterator
'symbol'

Apparently Symbol.iterator has the type symbol. If you learned JavaScript before 2015, this type didn’t exist. It seems that symbol is the only new JavaScript type that has been added since JavaScript was created! The others are object, function, string, number, boolean, and undefined.

Why was it so important to create a fundamental new data type? Wouldn’t myIterable.iterator = ... be enough? No: this could clash with an existing property on the object called iterator. By introducing the new symbol type, the JavaScript designers were able to introduce myIterable[Symbol.iterator] without the possibility of this clashing with existing properties.

You can create your own symbols with the Symbol function:

$ node
> Symbol("foo")
Symbol(foo)
> typeof Symbol("foo")
'symbol'

Behind the scenes, there is a symbol registry. You can think of this registry as an array of strings:

const registry = ["Symbol.iterator", "foo"]

Every time you call Symbol(description), it pushes the new string description to the array, and gives you back a new symbol that contains the index of the latest string the registry. So, given the registry above, Symbol("bar") would modify the registry to ["Symbol.iterator", "foo", "bar"], then return a symbol containing the index 2.

Given a symbol, you can get the description that was passed in using .description. The expression s.description effectively returns registry[s]:

$ node
> x = Symbol("foo")
Symbol(foo)
> x.description
'foo'
> y = Symbol()
Symbol()
> y.description
undefined
> Symbol.iterator.description
'Symbol.iterator'

So far, you could simulate symbols yourself using an array, just like above - so why do they have their own new type? Symbols are useful because of what you can’t do with them. Given a symbol, you cannot get access to the symbol’s registry index. The only things you can do with a symbol are compare it to something else, or look it up in an object (or get the string description - an unusual operation).

Symbol comparison uses the symbols’ registry indexes; not the string descriptions! Try it out:

$ node
> foo1 = Symbol("foo")
Symbol(foo)
> foo2 = Symbol("foo")
Symbol(foo)
> foo1 === foo2
false
> foo1 === foo1
true

The above example added two new "foo" descriptions to the registry, so it would look like the following:

const registry = [..., "foo", "foo"]

So far, I’ve referred to “the registry”, but actually there are many. First, there is one registry per “JavaScript realm”. In the browser, this basically means one per page/tab. This is the registry you’re modifying when creating symbols with Symbol("foo"). But there is an additional table, the “global symbol registry”, which is shared between multiple realms. (Just how “global” it is seems to be undefined.)

The global registry is accessed with Symbol.for, which acts a bit like Symbol:

$ node
> foo1 = Symbol.for("foo")
Symbol(foo)
> foo2 = Symbol.for("foo")
Symbol(foo)
> foo3 = Symbol("foo")
Symbol(foo)
> foo1 === foo2
true
> foo1 === foo3
false

Notice that Symbol.for does not create a new symbol every time. If a symbol already exists with that description in the global registry, it will return that symbol. This means that, unlike the per-realm registries, the global registry has unique symbol descriptions.

So, actually, a symbol is not just an index. It also carries along with it a reference to the registry that it’s a member of. You can’t easily tell, given a symbol, which registry it’s a member of. But you can exploit what we know to determine whether a symbol is a member of the global registry:

> isGlobalSymbol = (s) => Symbol.for(s.description) === s
[Function: isGlobalSymbol]
> isGlobalSymbol(foo1)
true
> isGlobalSymbol(foo2)
true
> isGlobalSymbol(foo3)
false

Get updates on Twitter


More by Jim

Tagged #programming, #javascript. All content copyright James Fisher 2019. This post is not associated with my employer. Found an error? Edit this page.