Symbols

With ES6, for the first time in quite a while, a new primitive type has been added to JavaScript: the symbol. Unlike the other primitive types, however, symbols don’t have a literal form.

Here’s how you create a symbol:

  1. var sym = Symbol( "some optional description" );
  2. typeof sym; // "symbol"

Some things to note:

  • You cannot and should not use new with Symbol(..). It’s not a constructor, nor are you producing an object.
  • The parameter passed to Symbol(..) is optional. If passed, it should be a string that gives a friendly description for the symbol’s purpose.
  • The typeof output is a new value ("symbol") that is the primary way to identify a symbol.

The description, if provided, is solely used for the stringification representation of the symbol:

  1. sym.toString(); // "Symbol(some optional description)"

Similar to how primitive string values are not instances of String, symbols are also not instances of Symbol. If, for some reason, you want to construct a boxed wrapper object form of a symbol value, you can do the following:

  1. sym instanceof Symbol; // false
  2. var symObj = Object( sym );
  3. symObj instanceof Symbol; // true
  4. symObj.valueOf() === sym; // true

Note: symObj in this snippet is interchangeable with sym; either form can be used in all places symbols are utilized. There’s not much reason to use the boxed wrapper object form (symObj) instead of the primitive form (sym). Keeping with similar advice for other primitives, it’s probably best to prefer sym over symObj.

The internal value of a symbol itself — referred to as its name — is hidden from the code and cannot be obtained. You can think of this symbol value as an automatically generated, unique (within your application) string value.

But if the value is hidden and unobtainable, what’s the point of having a symbol at all?

The main point of a symbol is to create a string-like value that can’t collide with any other value. So, for example, consider using a symbol as a constant representing an event name:

  1. const EVT_LOGIN = Symbol( "event.login" );

You’d then use EVT_LOGIN in place of a generic string literal like "event.login":

  1. evthub.listen( EVT_LOGIN, function(data){
  2. // ..
  3. } );

The benefit here is that EVT_LOGIN holds a value that cannot be duplicated (accidentally or otherwise) by any other value, so it is impossible for there to be any confusion of which event is being dispatched or handled.

Note: Under the covers, the evthub utility assumed in the previous snippet would almost certainly be using the symbol value from the EVT_LOGIN argument directly as the property/key in some internal object (hash) that tracks event handlers. If evthub instead needed to use the symbol value as a real string, it would need to explicitly coerce with String(..) or toString(), as implicit string coercion of symbols is not allowed.

You may use a symbol directly as a property name/key in an object, such as a special property that you want to treat as hidden or meta in usage. It’s important to know that although you intend to treat it as such, it is not actually a hidden or untouchable property.

Consider this module that implements the singleton pattern behavior — that is, it only allows itself to be created once:

  1. const INSTANCE = Symbol( "instance" );
  2. function HappyFace() {
  3. if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];
  4. function smile() { .. }
  5. return HappyFace[INSTANCE] = {
  6. smile: smile
  7. };
  8. }
  9. var me = HappyFace(),
  10. you = HappyFace();
  11. me === you; // true

The INSTANCE symbol value here is a special, almost hidden, meta-like property stored statically on the HappyFace() function object.

It could alternatively have been a plain old property like __instance, and the behavior would have been identical. The usage of a symbol simply improves the metaprogramming style, keeping this INSTANCE property set apart from any other normal properties.

Symbol Registry

One mild downside to using symbols as in the last few examples is that the EVT_LOGIN and INSTANCE variables had to be stored in an outer scope (perhaps even the global scope), or otherwise somehow stored in a publicly available location, so that all parts of the code that need to use the symbols can access them.

To aid in organizing code with access to these symbols, you can create symbol values with the global symbol registry. For example:

  1. const EVT_LOGIN = Symbol.for( "event.login" );
  2. console.log( EVT_LOGIN ); // Symbol(event.login)

And:

  1. function HappyFace() {
  2. const INSTANCE = Symbol.for( "instance" );
  3. if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];
  4. // ..
  5. return HappyFace[INSTANCE] = { .. };
  6. }

Symbol.for(..) looks in the global symbol registry to see if a symbol is already stored with the provided description text, and returns it if so. If not, it creates one to return. In other words, the global symbol registry treats symbol values, by description text, as singletons themselves.

But that also means that any part of your application can retrieve the symbol from the registry using Symbol.for(..), as long as the matching description name is used.

Ironically, symbols are basically intended to replace the use of magic strings (arbitrary string values given special meaning) in your application. But you precisely use magic description string values to uniquely identify/locate them in the global symbol registry!

To avoid accidental collisions, you’ll probably want to make your symbol descriptions quite unique. One easy way of doing that is to include prefix/context/namespacing information in them.

For example, consider a utility such as the following:

  1. function extractValues(str) {
  2. var key = Symbol.for( "extractValues.parse" ),
  3. re = extractValues[key] ||
  4. /[^=&]+?=([^&]+?)(?=&|$)/g,
  5. values = [], match;
  6. while (match = re.exec( str )) {
  7. values.push( match[1] );
  8. }
  9. return values;
  10. }

We use the magic string value "extractValues.parse" because it’s quite unlikely that any other symbol in the registry would ever collide with that description.

If a user of this utility wants to override the parsing regular expression, they can also use the symbol registry:

  1. extractValues[Symbol.for( "extractValues.parse" )] =
  2. /..some pattern../g;
  3. extractValues( "..some string.." );

Aside from the assistance the symbol registry provides in globally storing these values, everything we’re seeing here could have been done by just actually using the magic string "extractValues.parse" as the key, rather than the symbol. The improvements exist at the metaprogramming level more than the functional level.

You may have occasion to use a symbol value that has been stored in the registry to look up what description text (key) it’s stored under. For example, you may need to signal to another part of your application how to locate a symbol in the registry because you cannot pass the symbol value itself.

You can retrieve a registered symbol’s description text (key) using Symbol.keyFor(..):

  1. var s = Symbol.for( "something cool" );
  2. var desc = Symbol.keyFor( s );
  3. console.log( desc ); // "something cool"
  4. // get the symbol from the registry again
  5. var s2 = Symbol.for( desc );
  6. s2 === s; // true

Symbols as Object Properties

If a symbol is used as a property/key of an object, it’s stored in a special way so that the property will not show up in a normal enumeration of the object’s properties:

  1. var o = {
  2. foo: 42,
  3. [ Symbol( "bar" ) ]: "hello world",
  4. baz: true
  5. };
  6. Object.getOwnPropertyNames( o ); // [ "foo","baz" ]

To retrieve an object’s symbol properties:

  1. Object.getOwnPropertySymbols( o ); // [ Symbol(bar) ]

This makes it clear that a property symbol is not actually hidden or inaccessible, as you can always see it in the Object.getOwnPropertySymbols(..) list.

Built-In Symbols

ES6 comes with a number of predefined built-in symbols that expose various meta behaviors on JavaScript object values. However, these symbols are not registered in the global symbol registry, as one might expect.

Instead, they’re stored as properties on the Symbol function object. For example, in the “for..of“ section earlier in this chapter, we introduced the Symbol.iterator value:

  1. var a = [1,2,3];
  2. a[Symbol.iterator]; // native function

The specification uses the @@ prefix notation to refer to the built-in symbols, the most common ones being: @@iterator, @@toStringTag, @@toPrimitive. Several others are defined as well, though they probably won’t be used as often.

Note: See “Well Known Symbols” in Chapter 7 for detailed information about how these built-in symbols are used for meta programming purposes.