I just want to take a minute to acknowledge something that I
think is quite clever in ECMAScript 6, and that's the way that
the new Symbol
type is used. But first, let's get
the pedantry out of the way. If you're not feeling pedantic,
feel free to skip this next paragraph.
Yes, I know that the title is technically not fully correct. For the purposes of this post, I'm talking about the 6th edition of the ECMAScript standard, published in 2015. So it's not entirely, pedantically correct to say "Javascript" did anything, as it was the people behind the ECMAScript standard who came up with and implemented the idea for the things I'll be talking about here. I understand all of that, and I willfully choose to ignore it all, because the current title is just easier to say than "ECMAScript did something clever". Got it? Great! Moving on.
So what I want to talk about is symbols. Prior to ES6, keys on an object were always strings. Even if you thought you were using a number to index a field in some object, internally it was being stringified. Don't believe me? If you have Node.js installed, try the following interactions.
> foo = {}
{}
> foo[1] = "Numerical slot"
'Numerical slot'
> foo[1]
'Numerical slot'
> foo["1"]
'Numerical slot'
> foo
{ '1': 'Numerical slot' }
So internally, the number is being converted to a string. I belabor this point to make it clear that strings used to be the only things that could be used as field keys.
But this is not so in ES6 and beyond. A new type has been
defined, called Symbol
. In addition to strings,
symbols can now be used as field keys. The simplest way to
make a symbol is to use the interning
form: Symbol.for(...)
> foo = {}
{}
> foo[1] = "Numerical slot"
'Numerical slot'
> foo[Symbol.for('1')] = "Symbolic slot"
'Symbolic slot'
> foo
{ '1': 'Numerical slot' }
> foo[Symbol.for('1')]
'Symbolic slot'
So now we have something that's actually
different. Symbol.for('1')
is a new symbol that's
distinct from the ordinary string "1"
. Also note
that the symbolic field was not listed in the default
stringification for the object.
In addition to providing interned symbols, we can also make
new ones, simply by calling Symbol
directly.
> sym = Symbol()
Symbol()
> sym == sym
true
> Symbol() == Symbol()
false
As expected on that last line, the two new symbols are distinct from one another. We can use these new "made-up" symbols as keys on objects if we want, too.
> sym = Symbol()
Symbol()
> foo = {}
{}
> foo[sym] = 10
10
> foo[sym]
10
While this particular feature may not be immediately intuitive
or familiar to a lot of programmers, it's not new to the
field. Common Lisp has had a similar construct for awhile,
under the name gensym
, a function which produces
a brand new symbol.
[1]> (gensym)
#:G7193
[2]> (eq (gensym) (gensym))
NIL
[3]> (let ((foo (gensym))) (eq foo foo))
T
Okay, admittedly, that may look a bit terrifying to
non-Lispers, but we basically did the same thing here that we
just did in Javascript. gensym
makes a new
symbol. Two distinct gensym
'd symbols compare
unequal, but the same symbol always compares equal to
itself.1
So we have symbols, and we have a way of making new, made-up symbols. Like I said, Lisp did all that 40 years ago. No, the truly clever thing is how Javascript uses these symbols.
To frame the problem, let's say we want to make some objects
iterable. In a lot of languages, this is done by, say,
implementing some Iterable
interface and writing
an iterator()
method. Well, Javascript doesn't
have interfaces, so we'll cut out the middle man and just
have iterator()
.
The problem with this in Javascript is that people frequently do funny things with objects. Since objects are basically glorified dictionaries, people are often inclined to use them as dictionaries. Maybe I'm using an object to store all of the players in my new revolutionary MMO, and I use the player's last name as the key.
> foo
{ smith: ..., banks: ..., 'ward-sachs': ... }
This is all fine and good, but what happens when my good friend Johnny Iterator shows up to try out my game?
> foo
{ smith: ..., banks: ..., 'ward-sachs': ..., iterator: ... }
Uh-oh! Now my fancy database is iterable, and my poor friend
Johnny has been resigned to iterating over it. If we ever try
to use for ... of
on my object, it'll behave very
strangely. This goes double if I already had a method
named iterator
which actually produced an
iterator, and then Johnny came along and overwrote it with his
last name.
So we can't use strings. If only we had some way of making up
new symbols that nobody else was using... Well, that's exactly
what ES6 does. There are several "special" symbols (the formal
term is "well-known symbols", but I'm still having trouble
taking that term seriously as an official notion), one of
which is actually Symbol.iterator
, the symbol
used for iterators.
Now, rather than arbitrarily looking for
an iterator
field when we need to iterate, we can
look specifically for Symbol.iterator
, a made-up
symbol. And Johnny's last name can't possibly be this symbol,
since the Javascript runtime just now made up that symbol, and
I'm pretty sure Johnny's last name was decided before
the most recent execution of the Javascript runtime.
I don't really have a ton more to say. There are several well-known symbols defined in ES6. A full list can be found on MDN. I think it's a really clever solution to the problem of namespace pollution, and I'm considering implementing a similar technique for certain handlers in my own prototype-oriented programming language.
T
and NIL
are Common Lisp's way
of saying, respectively, true and false. These values
actually mean several things in Common Lisp and have some
interesting philosophical implications, but that's a topic
for another post.