A practical guide to ES6 Symbol

May 13, 2019 0 Comments

A practical guide to ES6 Symbol

 

 

Originally meant to introduce private properties to ES6, Symbols offer an approach to metaprogramming in Javascript that provides extension hooks into language operators and methods without risking user name collisions. A symbol is a primitive data type that is immutable and globally-unique.

There are three kinds of Symbols:

  • User-defined symbols created with the Symbol function
  • Globally-registered symbols created with the Symbol.for function
  • Well-known symbols defined as static properties on the Symbol object

While Symbols provide hooks into language methods, this same feature can be exploited by the developers of libraries and frameworks as well.

Here is a quick summary of how Symbols are used in code.

A user-defined symbol is created with the global Symbol function

They are new to ES6, but not that new

No two symbols are alike

Except when they are equal (i.e. symbols in the global registry)

They really are their own type

They can be set as properties on objects

They can usually be overwritten, even in native object prototypes

They can keep data from prying eyes

Except they are still discoverable through reflection

These are user-defined symbols, originally meant to introduce near-private properties to Javascript. Then there are built-in symbols (aka well-known symbols) designed to provide hooks into the implementation of native functions without risking clashes with user-defined names.

Some Symbols are more well-known than others. There are symbols like Symbol.iterator that allow developers control over how an object can be iterated and spread. Then there are symbols like Symbol.unscopables, which was born out of a need to maintain backwards compatibility with the seldom used with keyword.

As an example, consider how to write a Password class that:

  • Accepts an unmasked password string
  • Irreversibly masks the password
  • Can match (true or false) against a test password

In writing this class, we will use built-in symbols.

To simplicity sake, we will implement the actually “hashing” using an insecure method based on Java’s Object.hashCode function. For secure hashing, consider using a dedicated cryptography library like CryptoJs.

hashCode irreversibly turns a String into a Number with some probability of collision (i.e. two Strings hash into the same Number).

With our hashCode function, the first task is to mask the real password.

As we’ve seen before, a Symbol can be used as a property on an Object and is generally more private than say a String property prefixed with an underscore. However, it is not completely private. We can either use a direct reference to PWD or use reflective methods like Object.getOwnPropertySymbols to obtain a reference to our unique symbol.

For more private properties, we can use IIFE or WeakMap and soon ES7 will support a language-level private field modifier #.

The next task is to make Passwords comparable. Symbol.match provides a hook into String.match, which is one way for comparing strings. It is generally used to compare against a regular expression and the native implementation even implicitly coerces values into RegExp. However, our Password class will explicitly define its own behavior.

Now we can compare using the Password as we would a RegExp!

While the example above could be achieved without Symbols (either as its own method or by overwriting String.prototype), there are well-known Symbols that alter language and operator behavior like type conversion.

This is where Symbol.toPrimitive comes in. It is used to convert Objects to primitives and is called by operators like the unary plus operator. We will use this Symbol to convert our Password into a masked String.

Now when we coerce our Password into a String, we get asterisks.

The hint parameter hint can be “number”, “string”, or “default”. A more complete implementation could take these values into account and return NaN or other values, as applicable. For now, we will only support String.

When we try to turn our password back into a string we end up with a masked password with asterisks replacing each character.

It is worth noting that Symbol.toPrimitive is used for more than explicit type conversion. It is also used for implicit coercions done by the additional operator + as well as the equality operator ==.

You might notice that if instead we call Object.toString on our password, we get the generic tag [object Object]. Symbols allow us to be more descriptive. Using the Symbol.toStringTag symbol, we can provide the Javascript runtime with a better tag for our object.

Interestingly, Symbol.toStringTag is “a string valued property,” not a method. That means we need to use the get keyword in our password class (or Object.defineProperty on an instance) to declare this property.

Now when we call toString on our password, we get [object Password]. Although this is not something many apps would need, it can be useful from a debugging and logging perspective.

Another less common, but still powerful Symbol is Symbol.hasInstance. It controls the behavior of the instanceof operator.

Fortunately it is not possible to redefine native implementations of the Symbol.hasInstance function like Function.prototype[Symbol.hasInstance]. However, it is possible to do so on custom classes and objects like Password.

The main use for Symbol.hasInstance is in libraries that want more control over instanceof checks. For example, GraphQL JS uses Symbol.hasInstance to check for specific symbols that identify GraphQLTypes.

Symbol.iterator is among the most well-known and useful symbols. It enables user-defined iterables that implement the iteration protocol.

Here is an example of Symbol.iterator's power. They can be used to extend or override native object’s prototype. That means with just a few lines, we can define iteration for Objects globally.

Whether you should is a different question.

With this code, Objects can be spread into Arrays of [key, value] pairs. Likewise, Objects can be iterated using for...of loops.

In the context of our Password class, we don’t even have to write our own iterator! Since a Password can be coerced into a String, we can use the native String iterator directly.

Doing so allows us to spread or iterate a Password as a sequence of asterisks just as we could with a String.

Putting it all together, our final Password class looks like:

And has some interesting properties:

Future Symbols

The result is a class that masks the original password, is comparable, iterable, and convertible as we might expect from a String. While I have introduced five well-known Symbols, at the time of writing (May 2019) there are actually thirteen! The full list includes:

  1. Symbol​.async​Iterator
  2. Symbol​.has​Instance
  3. Symbol​.isConcat​Spreadable
  4. Symbol.iterator
  5. Symbol​.match
  6. Symbol​.matchAll
  7. Symbol​.replace
  8. Symbol​.search
  9. Symbol​.species
  10. Symbol​.split
  11. Symbol​.toPrimitive
  12. Symbol​.toStringTag
  13. Symbol​.unscopables

Be on the lookout for more soon! There are several suggestions and proposals for Private Symbols, Symbol.thenable, Symbol.isAbstractEquals, Symbol.equals, Symbol.inspect, Symbol.inObject, and more.

Symbols are a unique, immutable data type unlike their counterparts in other languages. As the Javascript language continues to evolve, I expect Symbols to become a mainstay in Javascript metaprogramming.

If you found this helpful, please give some claps 👏 👏 👏

You can find me on LinkedIn · GitHub · Medium


Tag cloud