Symbols in ECMAScript 6
Symbols are a new primitive type in ECMAScript 6 [1]. This blog post explains how they work.
A new primitive type
ECMAScript 6 introduces a new primitive type: symbols. They are tokens that serve as unique IDs. You create symbols via the factory function Symbol()
(which is loosely similar to String
returning strings if called as a function):
let symbol1 = Symbol();
Symbol()
has an optional string-valued parameter that lets you give the newly created symbol a description:
> let symbol2 = Symbol('symbol2');
> String(symbol2)
'Symbol(symbol2)'
Every symbol returned by Symbol()
is unique, every symbol has its own identity:
> symbol1 === symbol2
false
You can see that symbols are primitive if you apply the typeof
operator to one of them – it will return a new symbol-specific result:
> typeof symbol1
'symbol'
Aside: Two quick ideas of mine. If a symbol has no description, JavaScript engines could use the name of the variable (or property) that a symbol is assigned to. Minifiers could also help, by turning the original name of a variable into a parameter for Symbol
.
Symbols as property keys
Symbols can be used as property keys:
const MY_KEY = Symbol();
let obj = {};
obj[MY_KEY] = 123;
console.log(obj[MY_KEY]); // 123
Classes and object literals have a feature called computed property keys [2]: You can specify the key of a property via an expression, by putting it in square brackets. In the following object literal, we use a computed property key to make the value of MY_KEY
the key of a property.
const MY_KEY = Symbol();
let obj = {
[MY_KEY]: 123
};
A method definition can also have a computed key:
const FOO = Symbol();
let obj = {
[FOO]() {
return 'bar';
}
};
console.log(obj[FOO]()); // bar
Enumerating own property keys
Given that there is now a new kind of value that can become the key of a property, the following terminology is used for ECMAScript 6:
- Property keys are either strings or symbols.
- Property names are strings.
Let’s examine the API for enumerating own property keys by first creating an object.
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Object.defineProperty(obj,
'nonEnum', { enumerable: false });
Object.getOwnPropertyNames()
ignores symbol-valued property keys:
> Object.getOwnPropertyNames(obj)
['enum', 'nonEnum']
Object.getOwnPropertySymbols()
ignores string-valued property keys:
> Object.getOwnPropertySymbols(obj)
[Symbol(my_key)]
Reflect.ownKeys()
considers all kinds of keys:
> Reflect.ownKeys(obj)
[Symbol(my_key), 'enum', 'nonEnum']
The name of Object.keys()
doesn’t really work, anymore: it only considers enumerable property keys that are strings.
> Object.keys(obj)
['enum']
Using symbols to represent concepts
In ECMAScript 5, one often represents concepts (think enum constants) via strings. For example:
var COLOR_RED = 'RED';
var COLOR_ORANGE = 'ORANGE';
var COLOR_YELLOW = 'YELLOW';
var COLOR_GREEN = 'GREEN';
var COLOR_BLUE = 'BLUE';
var COLOR_VIOLET = 'VIOLET';
However, strings are not as unique as we’d like them to be. To see why, let’s look at the following function.
function getComplement(color) {
switch (color) {
case COLOR_RED:
return COLOR_GREEN;
case COLOR_ORANGE:
return COLOR_BLUE;
case COLOR_YELLOW:
return COLOR_VIOLET;
case COLOR_GREEN:
return COLOR_RED;
case COLOR_BLUE:
return COLOR_ORANGE;
case COLOR_VIOLET:
return COLOR_YELLOW;
default:
throw new Exception('Unknown color: '+color);
}
}
It is noteworthy that you can use arbitrary expressions as switch
cases, you are not limited in any way. For example:
function isThree(x) {
switch (x) {
case 1 + 1 + 1:
return true;
default:
return false;
}
}
We use the flexibility that switch
offers us and refer to the colors via our constants (COLOR_RED
etc.) instead of hard-coding them ('RED'
etc.).
Interestingly, even though we do so, there can still be mix-ups. For example, someone may define a constant for a mood:
var MOOD_BLUE = 'BLUE';
Now the value of BLUE
is not unique anymore and MOOD_BLUE
can be mistaken for it. If you use it as a parameter for getComplement()
, it returns 'ORANGE'
where it should throw an exception.
Let’s use symbols to fix this example. Now we can also use the ECMAScript 6 feature const
, which lets us declare actual constants (you can’t change what value is bound to a constant, but the value itself may be mutable).
const COLOR_RED = Symbol();
const COLOR_ORANGE = Symbol();
const COLOR_YELLOW = Symbol();
const COLOR_GREEN = Symbol();
const COLOR_BLUE = Symbol();
const COLOR_VIOLET = Symbol();
Each value returned by Symbol
is unique, which is why no other value can be mistaken for BLUE
now. Intriguingly, the code of getComplement()
doesn’t change at all if we use symbols instead of strings, which shows how similar they are.
Symbols as keys of properties
Being able to create properties whose keys never clash with other keys is useful in two situations:
- If several parties contribute internal properties to the same object, via mixins.
- To keep meta-level properties from clashing with base-level properties.
Symbols as keys of internal properties
Mixins are object fragments (sets of methods) that you can compose to augment the functionality of an object or a prototype. If their methods have symbols as keys, they can’t clash with other methods (of other mixins or of the object that they are added to), anymore.
Public methods are seen by clients of the object a mixin is added to. For usability’s sake, you probably want those methods to have string keys. Internal methods are only known to the mixin or only needed to communicate with it. They profit from having symbols as keys.
Symbols do not offer real privacy, because it is easy to find out the symbol-valued property keys of an object. But the guarantee that a property key can’t ever clash with any other property key is often enough. If you truly want to prevent the outside from accessing private data, you need to use WeakMaps or closures. For example:
// One WeakMap per private property
const PASSWORD = new WeakMap();
class Login {
constructor(name, password) {
this.name = name;
PASSWORD.set(this, password);
}
hasPassword(pw) {
return PASSWORD.get(this) === pw;
}
}
The instances of Login
are keys in the WeakMap PASSWORD
. The WeakMap does not prevent the instances from being garbage-collected. Entries whose keys are objects that don’t exist anymore are removed from WeakMaps.
The same code looks as follows if you use a symbol key for the internal property.
const PASSWORD = Symbol();
class Login {
constructor(name, password) {
this.name = name;
this[PASSWORD] = password;
}
hasPassword(pw) {
return this[PASSWORD] === pw;
}
}
Symbols as keys of meta-level properties
Symbols having unique identities makes them ideal as keys of public properties that exist on a different level than “normal” property keys, because meta-level keys and normal keys must not clash. One example of meta-level properties are methods that objects can implement to customize how they are treated by a library. Using symbol keys protect the library from mistaking normal methods as customization methods.
Iterability [3] in ECMAScript 6 is one such customization. An object is iterable if it has a method whose key is the symbol (stored in) Symbol.iterator
. In the following code, obj
is iterable.
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++]
};
} else {
return { done: true };
}
}
};
}
};
The iterability of obj
enables you to use the for-of
loop and similar JavaScript features:
for (let x of obj) {
console.log(x);
}
Crossing realms with symbols
A code realm (short: realm) is a context in which pieces of code exist. It includes global variables, loaded modules and more. Even though code exists “inside” exactly one realm, it may have access to code in other realms. For example, each frame in a browser has its own realm. And execution can jump from one frame to another, as the following HTML demonstrates.
<head>
<script>
function test(arr) {
var iframe = frames[0];
// This code and the iframe’s code exist in
// different realms. Therefore, global variables
// such as Array are different:
console.log(Array === iframe.Array); // false
console.log(arr instanceof Array); // false
console.log(arr instanceof iframe.Array); // true
// But: symbols are the same
console.log(Symbol.iterator ===
iframe.Symbol.iterator); // true
}
</script>
</head>
<body>
<iframe srcdoc="<script>window.parent.test([])</script>">
</iframe>
</body>
The problem is that each realm has its own local copy of Array
and, because objects have individual identities, those local copies are considered different, even though they are essentially the same object. Similarly, libraries and user code a loaded once per realm and each realm has a different version of the same object.
In contrast, members of the primitive types boolean, number and string don’t have individual identities and multiple copies of the same value are not a problem: The copies are compared “by value” (by looking at the content, not at the identity) and are considered equal.
Symbols have individual identities and thus don’t travel across realms as smoothly as other primitive values. That is a problem for symbols such as Symbol.iterator
that should work across realms: If an object is iterable in one realm, it should be iterable in others, too. If a cross-realm symbol is provided by the JavaScript engine, the engine can make sure that the same value is used in each realm. For libraries, however, we need extra support, which comes in the form of the global symbol registry: This registry is global to all realms and maps strings to symbols. For each symbol, libraries need to come up with a string that is as unique as possible. To create the symbol, they don’t use Symbol()
, they ask the registry for the symbol that the string is mapped to. If the registry already has an entry for the string, the associated symbol is returned. Otherwise, entry and symbol are created first.
You ask the registry for a symbol via Symbol.for()
and retrieve the string associated with a symbol (its key) via Symbol.keyFor()
:
> let sym = Symbol.for('Hello everybody!');
> Symbol.keyFor(sym)
'Hello everybody!'
As expected, cross-realm symbols, such as Symbol.iterator
, that are provided by the JavaScript engine are not in the registry:
> Symbol.keyFor(Symbol.iterator)
undefined
Safety checks
JavaScript warns you about two mistakes by throwing exceptions: Invoking Symbol
as a constructor and coercing symbols to string.
Invoking Symbol as a constructor
While all other primitive values have literals, you need to create symbols by function-calling Symbol
. Thus, it is relatively easy to accidentally invoke Symbol
as a constructor. That produces instances of Symbol
and is not very useful. Therefore, an exception is thrown when you try to do that:
> new Symbol()
TypeError: Symbol is not a constructor
There is still a way to create wrapper objects, instances of Symbol
: Object
, called as a function, converts all values to objects, including symbols.
> let sym = Symbol();
> typeof sym
'symbol'
> let wrapper = Object(sym);
> typeof wrapper
'object'
> wrapper instanceof Symbol
true
Coercing a symbol to string
Given that both strings and symbols can be property keys, you want to protect people from accidentally converting a symbol to a string. For example, like this:
let propertyKey = '__' + anotherPropertyKey;
ECMAScript 6 throws an exception if one uses implicit conversion to string (handled internally via the ToString operation):
> var sym = Symbol('My symbol');
> '' + sym
TypeError: Cannot convert a Symbol value to a string
However, you can still explicitly convert symbols to strings:
> String(sym)
'Symbol(My symbol)'
> sym.toString()
'Symbol(My symbol)'
Frequently asked questions
Are symbols primitives or objects?
In some ways, symbols are like primitive values, in other ways, they are like objects:
- Symbols are like strings (primitive values) w.r.t. what they are used for: as representations of concepts and as property keys.
- Symbols are like objects in that each symbol has its own identity.
The latter point can be illustrated by using objects as colors instead of symbols:
const COLOR_RED = Object.freeze({});
···
Optionally, you can make objects-as-symbols more minimal by freezing Object.create(null)
instead of {}
. Note that, in contrast to strings, objects can’t become property keys.
What are symbols then – primitive values or objects? In the end, they were turned into primitives, for two reasons.
First, symbols are more like strings than like objects: They are a fundamental value of the language, they are immutable and they can be used as property keys. Symbols having unique identities doesn’t necessarily contradict them being like strings: UUID algorithms produce strings that are quasi-unique.
Second, symbols are most often used as property keys, so it makes sense to optimize the JavaScript specification and the implementations for that use case. Then many abilities of objects are unnecessary:
- Objects can become prototypes of other objects.
- Wrapping an object with a proxy doesn’t change what it can be used for.
- Objects can be introspected: via
instanceof
,Object.keys()
, etc.
Them not having these abilities makes life easier for the specification and the implementations. There are also reports from the V8 team that when handling property keys, it is simpler to treat a primitive type differently than objects.
Aren’t strings enough?
In contrast to strings, symbols are unique and prevent name clashes. That is nice to have for tokens such as colors, but it is essential for supporting meta-level methods such as the one whose key is Symbol.iterator
. Python uses the special name __iter__
to avoid clashes. You can reserve double underscore names for programming language mechanisms, but what is a library to do? With symbols, we have an extensibility mechanism that works for everyone. As you can see later, in the section on public symbols, JavaScript itself already makes ample use of this mechanism.
There is one hypothetical alternative to symbols when it comes to clash-free property keys: use a naming convention. For example, strings with URLs (e.g. 'http://example.com/iterator'
). But that would introduce a second category of property keys (versus “normal” property names that are usually valid identifiers and don’t contain colons, slashes, dots, etc.), which is basically what symbols are, anyway. Then it is more elegant to explicitly turn those keys into a different kind of value.
The symbol API
This section gives an overview of the ECMAScript 6 API for symbols.
The function Symbol
Symbol(description?)
→symbol
Creates a new symbol. The optional parameterdescription
allows you to give the symbol a description, which is useful for debugging.
Symbol
is not intended to be used as a constructor – an exception is thrown if you invoke it via new
.
Public symbols
Several public symbols can be accessed via properties of Symbol
. They are all used as property keys and enable you to customize how JavaScript handles an object.
Customizing basic language operations:
Symbol.hasInstance
(method)
Lets an objectO
customize the behavior ofx instanceof O
.Symbol.toPrimitive
(method)
Lets an object customize how it is converted to a primitive value. This is the first step whenever something is coerced to a primitive type (via operators etc.).Symbol.toStringTag
(string)
Called byObject.prototype.toString
to compute the default string description of an objectobj
: '[object '+obj[Symbol.toStringTag]+']'.
Iteration [3]:
Symbol.iterator
(method)
Makes an object iterable. Returns an iterator.
Regular expressions: Four string methods are simply forwarded to their regular expression parameters. The methods that they are forwarded to have the following keys.
Symbol.match
is used byString.prototype.match
.Symbol.replace
is used byString.prototype.replace
.Symbol.search
is used byString.prototype.search
.Symbol.split
is used byString.prototype.split
.
Miscellaneous:
Symbol.unscopables
(Object)
Lets an object hide some properties from thewith
statement.Symbol.species
(method)
Helps with cloning typed arrays and instances ofRegExp
,ArrayBuffer
andPromise
.Symbol.isConcatSpreadable
(boolean)
Indicates whetherArray.prototype.concat
should concatenate the elements of an object or the object as an element.
Global symbol registry
If you want a symbol to be the same in all realms, you need to create it via the global symbol registry. The following method lets you do that:
Symbol.for(str)
→symbol
Returns the symbol whose key is the stringstr
in the registry. Ifstr
isn’t in the registry yet, a new symbol is created and filed in the registry under the keystr
.
Another method lets you make the reverse look up and found out under which key a string is stored in the registry. This is may be useful for serializing symbols.
Symbol.keyFor(sym)
→string
returns the string that is associated with the symbolsym
in the registry. Ifsym
isn’t in the registry, this method returnsundefined
.
Further reading
- Using ECMAScript 6 today
- ECMAScript 6: new OOP features besides classes
- Iterators and generators in ECMAScript 6
Comments
Post a Comment