Introduction to JavaScript “Symbols” and their use in Metaprogramming
By: Uday Hiwarale
Tags: Ecmascript, JavaScript, Symbols, Ecmascript 6, Nodejs

Well-known Symbols

What is the best way to create a symbol and share it across all code realms? Perhaps you would go with the Symbol.for() approach since symbols created by it are accessible across all realms. JavaScript also provides some predefined global symbols (runtime-wide) for specific use cases.

If an object property (own or inherited) has a special meaning, then using a string name for it is not such a good idea because it can be accidentally overridden as we have seen with the valueOf method of the previous lesson.

JavaScript has a lot of special inherited properties on objects such as valueOf and toString which should be overridden (but not accidentally) to provide custom object behavior. Let’s go through them once more.

Every object inheriting Object has the default implementation of the valueOf and the toString method since they are defined on the Object itself. The valueOf method is called when you perform an arithmetic operation on the object and toString method is called when a string representation of an object is needed.

The default toString implementation returns "[object Object]" string to indicate that it is a string representation of an Object value. The default valueOf implementation returns the object itself, hence any arithmetic operations return NaN since the final result would not be a number.

We can override these default implementation by providing a function value with the same property name either on the object itself or on its prototype chain. Therefore if you have a class, you can provide toString and valueOf instance methods to override these default behavior as shown below.

Symbol.toPrimitive

The problem with these special methods (properties) is that they can be easily overridden by mistake. In the case of toString and valueOf , the primitive value of the object (string or number) in certain contexts is split between these two methods and it can be confusing at times.

To solve these issues, JavaScript provides well-known symbols to be used as special property names one of which is toPrimitive symbol. All well-known symbols are exposed to the public as static properties of Symbol object (function actually) and they are shared in all code realms.

The Symbol.toPrimitive symbol does the job of toString and valueOf method at once and gets the preference over them. If an object has (own or inherited) method with the name Symbol.toPrimitive , it will be called with a hint indicating which primitive value of the object is demanded.

The hint argument of this method could be "number" , "string" or "default" depending on what operation is performed on the object. The "default" hint is used in the case of + operator as it can be used as arithmetic add operator where number is needed or as string concatenation operator where string is needed.

Symbol.toStringTag

As we have learned that addition of an object with a string value returns a weird string with format "[object Object]" . This happens because the toString method implementation of the Object returns this string.

However, this is not unique with the object (descendent of Object ) only. JavaScript implements this method for most of the classes.

💡 If you are weirded out by Undefined and Null , then don’t be. These constructor functions are not accessible to the runtime and they are purely implemented inside the JavaScript engine.

Since every single value in JavaScript is derived from a constructor (class) which is why we say everything in JavaScript is an object, every class has its own implementation to return a string that describes that object. If it doesn’t, then it will use Objects implementation.

As you can see from the above results, only the Tag part of the "[object <Tag>]" representation is changing. JavaScript gives us the power to change the value of Tag , perhaps to make things a little simple to understand.

The Symbol.toStringTab property on the object used as the Tag when toString method is executed on the object (implicitly or deliberately). You can provide this property on the object itself or on its prototype chain. If you are using a class, then you should use it as a getter.

Symbol.hasInstance

Imagine if you have two classes with some common fields. You would normally go for an inheritance pattern where a class extends another class to inherit common properties. But that’s not always available.

If you have a function that accepts an object and checks if that object is an instance of a specific class using the instanceof operator, then would create a a problem since even though an object might have properties of this specific class, it would not qualify this check unless it is derived from it.

In the above example, since employee object is not an instance of Person class nor the Employee class inherits the Person class, the instanceof operator returns false . To solve this issue, you need to put an OR condition to check if the object is an instance of Employee class as well.

But using a static method on a class with the name Symbol.hasInstance , you can override the default behavior of the instanceof operator. What you have to do is check the incoming object value and return a boolean value from within this method. Let’s see how this plays out.

Whenever <LHS> instanceof Person expression is evaluated, the Person[Symbol.hasInstance] method is called with LHS operand. In the above example, we have used in operator to check if name property exists in the instance or on its prototype chain since the only criteria for an object to be an instance of Person is the existance of the name property.

Symbol.isConcatSpreadable

You must have used [].concat() prototype method of the Array to create a new array by appending one or more items. The magical thing about concat method is that it flattens the arguments if an argument is an Array . The problem with that is you might not need that consistently.

You can avoid that by putting boolean property Symbol.isConcatSpreadable on an array (instance of Array ) or on its prototype. If this property exists and is set to false , the concat method will not flatten the array.

In the above example, the numbers and drivers arrays are not spreadable in concat operation, therefore, they remained as array values in the newArray , however, the sports array was spread since it doesn’t have the Symbol.inConcatSpreadable method on it or on its prototype.

Symbol.species

In the previous example, we defined the MyArray class that extends built-in Array class. The only thing MyArray implements on its own is the Symbol.isConcatSpreadable getter. Technically, any instance of MyArray inherits all the properties of the Array class.

However, there is a problem. If we use map , forEach or any prototype method of the Array class that returns a new array but on the instance of MyArray , we are going to get an instance of MyArray back which should be the desired output.

However, sometimes, you need to use a wrapper class only to provide additional behavior around a base class but keep the core functionality of the underlying base class consistent. For example, what we want is that when we use map or forEach on an instance of MyArray , it should return an Array .

The Symbol.species is a static getter property of a class that points to the constructor function (class) that should be used to derive objects such as from within map or forEach method in the case of Array .

As you can see in the above example, an implicit or explicit call of map method returned an instance of Array (rather than MyArray ).

💡 You can use the same approach for Set , Map , RegExp , Promise , TypedArray and ArrayBuffer classes as well.

Regular Expression Methods

Until now, when we talked about regular expressions, we meant an instance of RegExp . The same instance can be created from the literal expression in the form of /.../ . This regular expression object then can be used to match text patterns inside a string.

'google.com'.match(/^[a-z]+\.com$/gi) ▶ ["google.com"]

JavaScript with ES2015 makes it possible for any object to act like a regular expression object. Therefore you can pass a custom object in the str.match() call which will be used as a matcher just like a regular expression. This object should implement some well-known method that will be called to get back the result of the match operation.

When a matcher object has Symbol.match method, it can be used in String.prototype.match() method. This symbol method is invoked by JavaScript to obtain the result of str.match() .

In the above example, we have created a Matcher class that implements Symbol.match instance method, hence this method is available on the matcher object. When we call text.match(matcher) function, this method is executed with the text . We can use the text inside Symbol.match method to return a valid response.

💡 You also have the Symbol.matchAll well-known symbol to process String.prototype.matchAll() call.

The Symbol.search method of the matcher is executed when search prototype method is executed on a String . Similar to the Symbol.match , this method also receives the string as the argument onto which the search method was called.

The Symbol.replace method of the matcher is executed when replace prototype method is executed on a String . Since the replace prototype method needs a replacement text, this method is called with the original text and the replacement. You should return a string back from this method.

The Symbol.split method of the matcher is executed when split prototype method is executed on a String . You will get the original string as the argument and you are supposed to return an array from this method.

Symbol.iterator

We have talked about Symbol.iterator in a separate lesson but let’s summarize that lesson. ES2015 has introduced a new iteration protocol that contains guidelines to make any object iterable. An iterable object is an object which can be used in the for-of loop or in the spread operation.

💡 Until now, we could use only Array as an iterable while to iterate over an Object , you would need to use for-in loop. Also, you can’t spread an object natively, perhaps Object.values() or Object.entries() can help you.

Using the iterable protocol, you can make any object behave like an array in a for-of loop or in the spread operation. In these contexts, JavaScript first obtains an iterator from the iterable by calling the Symbol.iterator method of the iterable (could also be on its prototype).

This Symbol.iterator method is called at the beginning of the iteration and it should return an iterator object. This iterator object has the next method which returns an object with done boolean field and value field.

This iterator.next() method is called indefinitely in the iteration until done is false . This value field is what we are interested in. This field represents the value of each iteration. The when done is false , the value is ignored.

💡 Read more about the iteration protocol from the MDN documentation.

Designing iterators is not such a fun process because it involves a lot of boilerplate code as you can see above. That’s why JavaScript gives us another feature called Generator which is a special kind of function. These generator functions (or simply generators) when called return an iterator. Hence the Symbol.iterator property could be a generator as well.

When next method is called on the iterator returned by the generator, each yield expression is evaluated which produces an iteration object with done and value fields. Once all yield expressions are evaluated, the iterator returns an iteration object with done set to false whenever the next method is called.

The *[ Symbol.iterator ] method supplies a generator which is why we needed to provide * at the beginning of the method name. JavaScript implements iterator protocols for multiple classes (by default) such as Array , Set , Map , String , TypedArray, etc. as shown below.

typeof Array.prototype[Symbol.iterator]; // 'function'

typeof String.prototype[Symbol.iterator]; // 'function'

typeof Map.prototype[Symbol.iterator]; // 'function'

typeof Set.prototype[Symbol.iterator]; // 'function'

typeof Uint8Array.prototype[Symbol.iterator]; // 'function' // hence you can spread a string

let vowels = [...'aeiou']; // [ 'a', 'e', 'i', 'o', 'u' ]

Symbol.asyncIterator

JavaScript support another variation of the for-of loop for the iteration of promises in a synchronous manner. The for-await-of loop can iterate over an iterable in a synchronous manner that returns a promise for each iteration.

In the above example, the promises array contains a list of promises that resolves with a string value after a few milliseconds. If we run a normal for-of loop on it, each iteration will receive the promise as the value. But what we need is the resolved value of each promise.

The for await syntax makes it possible. It waits until each promise is resolved and the result variable gets the resolved value of the promise. Since we are using the await keyword, we need to put for-await-of loop inside an async function, which could be an IIFE, it doesn’t matter.

Just like for-of loop, the for-async-of loop also uses Symbol.iterator method of the iterable to receive an iterator. If iterator returns a promise for each iteration, the result would be the same as the above.

However, for-await-of loop prefers Symbol.asyncIterator method if available on the iterable. This method also works like Symbol.iterator but it’s an async method, therefore, you can use await keyword inside the generator (or custom method that returns an iterator).

In the above example, each yield expression will await the promise resolution so that it can supply the resolved value of the promise as the value of the iteration. However, you can also yield the promise itself (just drop the await keyword) and things will work in the same manner.

What is @@iterator ?

You probably have seen @@iterator notation in JavaScript documentations such as MDN’s Array documentation or in perhaps in the console logs or stack traces. The @@ symbol is a specification prefix for well-known symbols and it doesn’t have any significance at runtime.

The below table is an official ES2021 table of well-known symbols. On the left column, you can find the specification name of the well-known symbol while on the right column is the actual symbol accessible at runtime.

💡 The Symbol.unscopables well-known symbol is used to control the behavior of an object when used inside with statement. But since with statement is not really recommended and it is kind of controversial, you should avoid using it.

What are symbols good for?

Well, this is the most difficult section of the whole article. I would say symbols are great to avoid non-standard consumption of your API. For example, if you do not want people to accidentally override an object property, make it a symbol and expose it through a global object or make it a global symbol.

Another thing symbols are good for is to represent a unique value. For example, if you want to create an enum to represent a fixed set of unique values, create an object with symbol values.