JavaScript Symbols

Chapter 3 13 mins

Learning outcomes:

  1. What are symbols
  2. Working with symbols
  3. The Object.getOwnPropertySymbols() method
  4. Global symbol registry

Introduction

For a long time, discussion has remained amongst developers of JavaScript of providing a feature that enables creating hidden properties on objects.

The properties will only be accessible by the code of the object on which they are defined - not anywhere else!

The idea sounded well, and even sounds quite well here, but the thing is that owing to the architecture of web browsers and JavaScript, nothing in the language can actually be made hidden!

Yup, that's true!

Even variables enclosed inside a closure are accessible, by means of developer tools. But yes the code directly doesn't have access to the variables inside the closure - it's just if some geek wants to go into it.

Nonetheless JavaScript still lacked a way to indicate that a bunch of properties on an object were meant to be used for internal sort-of-secret purposes only.

Thus stepped in symbols! Let's see what they are...

What are symbols?

At their core level:

Symbols are primitive values, unique in nature and meant to be used as property keys.

Elaborating on the definition, we see that symbols are primitive values i.e have no properties or methods attached to them directly.

Prior to the addition of symbols, JavaScript had five primitive data types: number, string, boolean, undefined and null. With the addition, it now has six (in fact, seven if we also include bigint).

Moving on, the symbol data type is meant to be used as property keys only.

For example, we can't use symbols in alert messages, or in calculating the total cost of our shopping list, or to group values into a collection like arrays.

Rather they were invented only to be utilised as keys of object properties. That's it!

We all know that objects have properties, and that properties are comprised of two things - a key and a value.

As we've been witnessing all this time, a property key can be of type "string" and now that we are learning symbols, of type "symbol" too.

We'll see below how to create symbols and use them for this very purpose.

Working with symbols

Let's start by seeing how to create a symbol and then move over to actually using it.

To create a symbol we call the Symbol() function:

var sym = Symbol(); // creates a new symbol

Unfortunately, unlike numbers, strings and booleans, symbols don't have a literal representation. They can only be created by calling functions - Symbol() in this case.

In the code above we create a variable sym and assign it to a symbol value that's created by calling Symbol().

Inspecting sym using the typeof operator returns "symbol", as can be seen below:

var sym = Symbol();
console.log(typeof sym); // "symbol"

With a symbol created we can now use it as an objects's property key.

Consider the following code. We have an object o with a property x (i.e the property key is the string "x"):

var o = { x: 10 };
var sym = Symbol();

To add a symbol key to this object o we'll need to use bracket notation, just how we use it for string keys.

The reason for bracket notation is simple:

Dot notation doesn't compute the property key - it goes as it is. In contrast, bracket notation first computes the values and then makes it a key of the object.

For example, obj.sym will create a string key "sym" on obj; whereas obj[sym] will create a symbol key.

Coming back to the topic, following we use the symbol sym to create a new property on o:

var o = { x: 10 };
var sym = Symbol();

o[sym] = "Symbols are amazing!";
console.log(o[sym]); // "Symbols are amazing!"

Now our object o has two properties - one with key of type string and the other with a key of type symbol.

Remember that we are talking about the types of the property keys - not the types of the property values!

The beauty of symbols is that they don't require any new special kind of notation if one needs to work with them - rather they merely believe on the fact that old is gold!

The Symbol() function can accept an optional description argument to describe the symbol being created.

This is particularly handy if we want to know the purpose of a symbol while inspecting it within a list of an object's properties. It also helps in debugging occasions.

However, remember that the description is in no way used in the actual algorithm for creating a symbol. It's just put in an internal [[Description]] slot of the symbol to be retrieved for inspection later.

Shown below is an example:

var sym = Symbol("OK");

We create a symbol sym with description "OK".

Upon inspecting this symbol in the console, we see that it logs Symbol("OK"). The string representation includes the description inside the parentheses:

var sym = Symbol("OK");
console.log(sym);
Symbol(OK)

An important point to note in this regards is that two symbols with the same description are not equal to one another. Recall that symbols are unique entities - just an exactly same description can't change this idea!

Consider the code below:

var sym1 = Symbol("OK");
var sym2 = Symbol("OK");

console.log(sym1 == sym2); // false
console.log(sym1 === sym2); // false

Both the symbols sym1 and sym2 have the same description, but are NOT equal to (can be seen in line 4), or identical to (can be seen in line 5) one another.

With the basics all done let's now move to some other aspects of symbols.

Getting symbol keys

Although symbol properties are enumerable the moment they are created, they dont' show up in many places.

Examples include the for...in loop, and the methods Object.keys() and Object.getOwnPropertyNames().

However, this doesn't mean that it's impossible to retrieve symbol keys from a given object - this is what Object.getOwnPropertySymbols() was created for.

The method Object.getOwnPropertySymbols() takes in an object argument and returns all its symbolic keys in the form of a list.

This is the only way to retrieve a list of all symbol properties from an object.

Take a look at the code below:

var o = {};

o[Symbol("sym1")] = "How";
o[Symbol("sym2")] = "are";
o[Symbol("sym3")] = "you?";

console.log(Object.getOwnPropertySymbols(o));
// [Symbol(sym1), Symbol(sym2), Symbol(sym3)]

We have created three symbol properties on an object o which we then retrieve from the list returned by Object.getOwnPropertySymbols().

As the name suggests, Object.getOwnPropertySymbols() returns a list containing only the symbols that directly belong to the given object.

Global symbol registry

When working in large applications, managing symbols across multiple files and contexts can be an error-prone task.

For instance, suppose you create a symbol sym, in an HTML document that has an iframe within it, and want the iframe to have access to that symbol.

The code of the iframe would look something similar to the following.

var sym = window.top.sym;

We will have to first refer to the top most window that belongs to the main HTML document and then get the symbol from there.

If no symbol sym exists in the main document, the variable sym inside the iframe will resolve down to undefined and have the potential to cause errors downstream.

Furthermore, suppose that you have an application that separately calls two scripts, both of which have to use the same syntax.

Managing the global symbols in both these scripts isn't impossible, but not without a lot of unnecessary code clutter.

As a solution to all these small instances of problems, JavaScript introduced something called the global symbol registry.

It's an environment shared by, and therefore accessible to, all contexts of an application i.e the main HTML document, all iframe windows and so on.

Two methods power up the symbol registry: Symbol.for() and Symbol.keyFor().

Symbol.for() takes in an argument, creates a symbol on the global registry with the argument as its key and finally returns the symbol. If a symbol already exists on the given key, it is returned instead.

Consider the code below:

var sym1 = Symbol.for("app");
console.log(sym1);

var sym2 = Symbol.for("app");

In line 1, a symbol is put up on the global registry with the key "app" and then logged in line 2.

Now when Symbol.for("app") is called again in line 4, since the key "app" already exists in the registry, its corresponding symbol is returned instead of creating a new one.

This can be confirmed by the following statement:

console.log(sym1 === sym2); // true

Both sym1 and sym2 are effectively the exact same symbol, and are therefore equal to one another.

Talking about the second method Symbol.keyFor(), it's the exact opposite of Symbol.for().

Symbol.keyFor() takes in a symbol argument and returns its corresponding key in the global registry, or else the value undefined.

Shown below is an example.

var sym = Symbol.for("app");

var key = Symbol.keyFor(sym);
console.log(key); // "app"

First we register a global symbol, saved in a variable sym, and then pass it on to the Symbol.keyFor() method.

The method returns the symbol's corresponding key in the registry, which we know is the value "app" and puts it in the variable key.

Symbols that don't exist in the global registry will simply yield undefined when passed to the keyFor() method:

var randomSym = Symbol("app");

var key = Symbol.keyFor(randomSym);
console.log(key); // undefined