symbol is a primitive data type that is immutable and globally-unique.
There are three kinds of Symbols:
- User-defined symbols created with the
- Globally-registered symbols created with the
- 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
They are new to ES6, but not that
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
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
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).
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.
The next task is to make
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
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 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.
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
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
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:
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:
If you found this helpful, please give some claps 👏 👏 👏