Quantcast
Channel: Stories by Uday Hiwarale on Medium
Viewing all articles
Browse latest Browse all 145

Introduction to JavaScript “Symbols” and their use in Metaprogramming

$
0
0

Metaprogramming in JavaScript

In this lesson, we are going to learn about JavaScript symbols and some of the new JavaScript features that depend on them. We will also discuss how they aid metaprogramming.

(source: unsplash.com)

What are the primitive data types in JavaScript? Well, they are null, undefined, string, number and boolean. Do we need more? Yes. The symbol is a new primitive data type introduced in ES2015 (ES6) along with bigint which was also introduced in this revision.

typeof null;      // 'object' (it's a bug)
typeof undefined; // 'undefined'
typeof 100; // 'number'
typeof "hello"; // 'string'
typeof true; // 'boolean'
typeof Symbol(); // 'symbol'
typeof 100n; // 'bigint'

In this lesson, we are just going to talk about symbols and leave others for another day. Symbols are fascinating and not like any other data type you have seen before. For the starters, the symbol is a primitive data type, however, you can’t write it in literal form because it doesn’t have one.

var sym = Symbol(description);

To create a symbol, you need to call the Symbol function as shown above. This function always returns a unique symbol value. The internal implementation of a symbol is up to the implementer (JavaScript engine) but it is neither a string, number, or a boolean value. Let’s see how it looks like.

(symbols/constructor.js)

The Symbol constructor always returns a new symbol. It accepts description argument which is used as a debugging aid while logging a symbol to the console and it shouldn’t be confused with the actual value of the symbol.

Two symbols can’t be unique if created from the Symbol() call since every Symbol() call, with or without a description, always returns a new symbol with its own unique value. Therefore a symbol can only be equal to itself.

Since symbol values are unique and not visible to the runtime, they cannot be represented in a literal form as you would normally do with strings or numbers. Hence there isn’t a magic syntax like var sym = #$@%#*#; that would make a symbol. You need to use Symbol() function call.

💡 The Symbol function, unlike any other function, isn’t constructible which means the expression new Symbol() will throw with TypeError: Symbol is not a constructor error however it does provide static methods as we will see later.

A symbol is allowed to be used as an object’s key. Since a symbol is not a string and it doesn’t have a literal form, we need to use the square-bracket notation ([]) as you would use with a variable that contains a string.

(symbols/object-keys.js)

In the above example, we have created sym symbol to represent the salary of the person object. The only way can get and set (or even delete) the value represented by a property name of the type symbol is by using the original symbol value (stored in a variable) and the [] notation.

If you ever lost the symbol value which represents a property in the object, it would be really tough to access its value in the object again. As you can see, using [Symbol()] syntax isn’t a wise solution because now you do not have access to the symbol that represents the property.

Object properties of the type symbol are not enumerable which means they do not show up in the result of Object.keys() method (own/enumerable properties) of an object and in the for-in loop (own+inherited/enumerable properties) as well as in the result of the Object.getOwnPropertyNames() method (own/enumerable+non-enumerable properties)

(symbols/non-enumerable.js)

I wouldn’t look at it as a drawback but a powerful abstraction feature. Since symbols are treated as non-enumerable properties, they can hold special meaning in the program. One must need to have the reference to the original symbol to override the value of a symbol property. This works perfectly well for a module that exposes an object with symbol property but not the symbol.

However, JavaScript provides Object.getOwnPropertySymbols() method that returns the list of own properties of an object which are symbols. So do not consider symbols would act like private properties. Symbols are just hard to access but not impossible.

(symbols/extract-symbol-props.js)

Oh, and one thing I didn’t tell you, every symbol has a property description that returns the description that was provided in the Symbol(description) call. If none was provided, it would be undefined. In the above example, we have used it as a hack to obtain the symbol value that was used as a property key to represent age, but this is not a clever approach.

So far one thing is very clear to us, a symbol is nothing like a string but that may pose a problem when a string value is expected from a symbol in some contexts. For example, what if a symbol is interpolated inside a string or what if an object with symbol properties is serialized into JSON.

(symbols/symbol-string-problems.js)

As you can see from the above example, JSON.stringify would simply ignore any property names that are symbols and serialize the rest of the fields without any errors. However, the operation that tries to read symbols as a string would result in TypeError.

💡 If you are wondering how console.log was able to display the symbol value as a string before, it’s because the console.log method calls the toString method on the symbol internally. You can also make the sym.toString() call manually which returns "Symbol(salary)" as a string.

Global Symbol Registry

I haven’t been entirely honest with you. There is a way you can access a symbol without having its reference or avoid creating unique symbols every single time. The Symbol.for(key) method returns a symbol from the global registry with the unique key, else it creates one on the fly and returns.

This global registry is common for every script file, every module, and every realm such as iframe, web worker, service worker, etc. For this reason, this registry is called the runtime-wide symbol registry. So when you access Symbol.for(key) in any realm, you get the same symbol every time.

The Symbol.keyFor(sym) method returns the key of a symbol only when a symbol is a runtime-wide symbol, else undefined is returned.

(symbols/runtime-wide-symbols.js)

When we create a symbol using Symbol.for(key) expression, the key becomes not only the unique identifier of the symbol but also its description which is nice as there is no other way to provide a description.

💡 If you are looking for a function to remove a symbol from the global registry then you should give up on your quest as it doesn’t exist.

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.

(symbols/special-methods.js)

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.

(symbols/special-methods-on-class.js)

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.

(symbols/symbol-toPrimitive.js)

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.

(symbols/objects-string-representation.js)
💡 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.

(symbols/symbol-toStringTag.js)

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.

(symbols/instanceof-problem.js)

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.

(symbols/symbol-hasInstance.js)

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.

(symbols/symbol-isConcatSpreadable.js)

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.

(symbols/array-extend.js)

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.

(symbols/symbol-species.js)

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().

(symbols/symbol-match.js)

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.

(symbols/symbol-search.js)

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.

(symbols/symbol-replace.js)

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.

(symbols/symbol-split.js)

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.

(symbols/symbol-iterator.js)

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.

(symbols/for-await-of.js)

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).

(symbols/symbol-async-iterator.js)

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.

(source: ECMAScript 2021)
💡 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.

// worst 😵
print( "red" );
----------------------
// bad 😔
var COLORS = {
RED: "RED",
GREEN: "GREEN",
BLUE: "BLUE"
};
print( COLOR.RED );
----------------------
// good 😃
var COLORS = {
RED: Symbol( 'COLORS.RED' ),
GREEN: Symbol( 'GREEN.GREEN' ),
BLUE: Symbol( 'COLORS.BLUE' )
};
print( COLOR.RED );
(Support Me on Patreon / GitHub / Twitter/ StackOverflow / Instagram)

Introduction to JavaScript “Symbols” and their use in Metaprogramming was originally published in JsPoint on Medium, where people are continuing the conversation by highlighting and responding to this story.


Viewing all articles
Browse latest Browse all 145

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>