My first encounter with Symbol in JavaScript was when I wanted to turn some classes in the ip-num library to iterables in other for them to be used in the "for of" syntax JavaScript provides.
The code involving Symbol that I ended up writing looked somewhat like this:
[Symbol.iterator](): IterableIterator<IPv4> { return this; }
You can see one of such places where this exist in the ip-num codebase here
Back then, this piece of code totally threw me off, as I have never encountered something like this previously in JavaScript, and I had to take some time out to dig into Symbol in order to understand what was going on.
This post is a jot down of some of the key things I got to learn about Symbol. It also marks the beginning of a series of posts on Iterables and Iterators in JavaScript.
This post on Symbol will contain the following:
Computed property keys
This post starts off by talking about computed property keys, because knowing about this, will help in deconstructing part of the syntax that had to do with Symbol and the part they play in iterables and Iterators in JavaScript.
One of the reasons why I was initially thrown off by the code involving Symbol was due to the fact that I did not appreciate this new part of JavaScript that was added in ES6.
One of the reasons why I was initially thrown off by the code involving Symbol was due to the fact that I did not appreciate this new part of JavaScript that was added in ES6.
[Symbol.iterator](): IterableIterator<IPv4> { return this; }
Not only did the Symbol.iterator looked unfamiliar, the syntax of it being between brackets was also confounding. Understanding the syntax was one of the steps towards deciphering Symbol and how it relates with iterables and iterators and this is what is explained next.
There are two ways to set and access properties of objects in Javascript. These two ways are the dot notation and the bracket notation. This is illustrated below:
//Setting var person = {}; person.firstname = John; // dot notation person['lastname'] = 'Agbabiaka'; // bracket notation let prop = 'salutation' person[prop] = 'Mr' // bracket notation //Accessing console.log(person.firstname) // dot notation console.log(person['lastname']) // bracket notation console.log(person[prop]) // bracket notation
Also, as you probably already know, creation of object is also possible using object literals, for example:
let personLiteral = { firstname: 'John', lastname: 'Agbabiaka' salutation: 'Mr' }
With object literals, properties are defined at the point of creation. What if we want properties to be dynamic? Instead of having them at point of creation, we want them to be computed? This is what computed property keys offer.
Prior to ES6, it was not possible to have such computed property keys in object literals. ES6, makes it possible. Hence the following is valid and syntactically correct in ES6:
let property_key = 'salutation' personLiteral = { firstname: 'John', // static property ['lastname']: 'Agbabiaka', // using computed property key directly [property_key]: 'Mr' // storing the property key first in a variable }
The syntax of the computed property key take the following format: [key]. Note the [] is part of the syntax and signifies bracket notation and has nothing to do with arrays.
To make it more obvious that we have a computed key, we can defined the 'salutation' property with a function:
This syntax can also be used with classes, to define not only properties, but also methods on classes:
And the Person class can be instantiated and its getFirstName method can then be called, also either via the dot notation, or the bracket notation:
After knowing about computed property keys, I was then able to start deciphering the piece of code:
Not all of it, but enough to see that a method is being defined, and the name of the method is computed via [Symbol.iterator].
But what exactly is Symbol.iterator? That would be discussed in this post, but before then, let us first understand what a Symbol is in JavaScript?
Symbols are a new primitive data type that was added in ES6. Symbol values are guaranteed to be unique and immutable and this is their core distinguishing feature.
If you may, you can think of them as some form of UUID; in the sense that creating one will always lead to a unique value that cannot be changed, and because of their guaranteed uniqueness, Symbol can be used as unique property names in objects. This ensures that such property name do not get mistakenly overridden somewhere else in the code.
For example if you have a shopping_cart object, you can have the property that holds the items in the shopping_cart as a symbol:
This way, the cart_items symbol property will never mistakenly clash with any other property that can be attached to the object later on.
Another thing to note with Symbol when used as properties of an object is their visibility. By default they are hidden and cannot be accessed in a for loop, via Object.keys nor Object.getOwnPropertyNames
For example:
This is not to say that the symbol property are totally hidden, no. To access the symbol that is the property of an object, use Object.getOwnPropertySymbols
So:
Creating non global Symbol values
In the previous section, we saw that a Symbol value can be created by calling the Symbol() function.
Symbols have no literals, hence the Symbol() function is the only way to create a Symbol. This is a departure from other primitive types (eg string, number etc) in JavaScript that have literals.
Also Symbol is not a Constructor function. To understand what constructor functions are, you can read a previous post: Understanding Constructor Function and this Keyword in Javascript
This means trying to create a symbol value by "new-ing" the Symbol() function will lead to a syntax error:
The Symbol() function can take a string argument. When such an argument is provided, it serves as a descriptive text for the symbol.
'This is sym1' is the descriptive text, and can be accessed from the variable via the description property. That is:
Creating global Symbol values
Normal scoping rules apply to values created via the Symbol() function, which is, they are scoped within the function they are created within. To have globally scoped symbol, there is Symbol.for('unique.key').
The Symbol.for mechanism creates global Symbols, that are indexed through the string passed in as arguments.
To illustrate let us consider a non global symbol created via the Symbol() function:
No way to access local_scoped_symbol, It is lost forever once the function goes out of scope.
If Symbol.for() is used, it would be possible to access the value of the symbol created even after the function goes out of scope. This is because the key created by Symbol.for() is globally available and not function scoped.
This is because when Symbol.for is used, if no value has been previously created with the given key, it is created and indexed in the global symbol registry. If a value has been created previously, the value is thus retrieved from the registry and returned.
These inbuilt Symbols serve as a form of language extension points, that allows developers to hook into inbuilt behaviour for their custom data structures.
They are like dunder methods (magic methods) in Python.
For example, JavaScript has a default string format when toString is called on its inbuilt data type:
But what if you want to define a custom class and have its instance printed, what will the output be?
Let’s see:
"[object Object]" is also printed when toString is called on instance of a custom class.
What if you want to modify this inbuilt behaviour? What if we want "[object Rectangle]" to be printed?
One way is to use the Symbol.toStringTag inbuilt symbol to specify the string value to be used for the default description of an object. So if you want to have "[object Rectangle]" printed, then the class definition needs to be modified to:
And this, is an example of how the inbuilt symbols can be used to hook into inbuilt language behaviour for custom data structures.
Symbol.iterator is part of the couple of inbuilt symbols available in JavaScript. It allows for hooking into the inbuilt for of iteration mechanism. This is why the Symbol.iterator comes into play when talking about iterables and iterators in JavaScript.
But how does it work? How does Symbol.iterator allow us to create custom data structures that can be iterated with for of syntax?
Before we get to that, we first need to look into the concepts of Iterators and Iterables. And this is exactly what the next post about.
let compute_key = () => 'salutation' // defined with a function personLiteral = { firstname: 'John', // static property ['lastname']: 'Agbabiaka', // using computed property key directly [compute_key()]: 'Mr' // storing the property key via a function call }
This syntax can also be used with classes, to define not only properties, but also methods on classes:
let prop = 'salutation' class Person { firstname = 'John'; ['lastname'] = 'Agbabiaka'; [prop] = 'Mr'; ['getFirstName']() { // computed key for method return this.firstname } }
And the Person class can be instantiated and its getFirstName method can then be called, also either via the dot notation, or the bracket notation:
new Person().getFirstName() new Person()['getFirstName']()
After knowing about computed property keys, I was then able to start deciphering the piece of code:
[Symbol.iterator](): IterableIterator<IPv4> { return this; }
Not all of it, but enough to see that a method is being defined, and the name of the method is computed via [Symbol.iterator].
But what exactly is Symbol.iterator? That would be discussed in this post, but before then, let us first understand what a Symbol is in JavaScript?
What Are Symbols
Symbols are a new primitive data type that was added in ES6. Symbol values are guaranteed to be unique and immutable and this is their core distinguishing feature.
let sym1 = Symbol() let sym2 = Symbol() console.log(sym1 == sym2) // false console.log(sym1 === sym2) // false
If you may, you can think of them as some form of UUID; in the sense that creating one will always lead to a unique value that cannot be changed, and because of their guaranteed uniqueness, Symbol can be used as unique property names in objects. This ensures that such property name do not get mistakenly overridden somewhere else in the code.
For example if you have a shopping_cart object, you can have the property that holds the items in the shopping_cart as a symbol:
let cart_items = Symbol('cart items') let shopping_cart = { name: "amazonia", [cart_items] : [] }
This way, the cart_items symbol property will never mistakenly clash with any other property that can be attached to the object later on.
Another thing to note with Symbol when used as properties of an object is their visibility. By default they are hidden and cannot be accessed in a for loop, via Object.keys nor Object.getOwnPropertyNames
For example:
for (prop in shopping_cart) { console.log(prop) // prints only "name" } console.log(Object.keys(shopping_cart) // prints ["name"] console.log(Object.getOwnPropertyNames(shopping_cart)) // prints ["name"]
This is not to say that the symbol property are totally hidden, no. To access the symbol that is the property of an object, use Object.getOwnPropertySymbols
So:
Object.getOwnPropertySymbols(shopping_cart) // [Symbol(cart items)]
Creating Symbol values
Creating non global Symbol values
In the previous section, we saw that a Symbol value can be created by calling the Symbol() function.
Symbols have no literals, hence the Symbol() function is the only way to create a Symbol. This is a departure from other primitive types (eg string, number etc) in JavaScript that have literals.
Also Symbol is not a Constructor function. To understand what constructor functions are, you can read a previous post: Understanding Constructor Function and this Keyword in Javascript
This means trying to create a symbol value by "new-ing" the Symbol() function will lead to a syntax error:
let sym2 = new Symbol() Uncaught TypeError: Symbol is not a constructor at new Symbol (<anonymous>) at <anonymous>:1:12
The Symbol() function can take a string argument. When such an argument is provided, it serves as a descriptive text for the symbol.
let sym1 = Symbol('This is sym1')
'This is sym1' is the descriptive text, and can be accessed from the variable via the description property. That is:
console.log(sym1.description) // prints 'This is sym1'
Creating global Symbol values
Normal scoping rules apply to values created via the Symbol() function, which is, they are scoped within the function they are created within. To have globally scoped symbol, there is Symbol.for('unique.key').
The Symbol.for mechanism creates global Symbols, that are indexed through the string passed in as arguments.
To illustrate let us consider a non global symbol created via the Symbol() function:
(function() { let local_scoped_symbol = Symbol(); })()
No way to access local_scoped_symbol, It is lost forever once the function goes out of scope.
If Symbol.for() is used, it would be possible to access the value of the symbol created even after the function goes out of scope. This is because the key created by Symbol.for() is globally available and not function scoped.
var obj = {}; (function() { let global_scoped_symbol = Symbol.for('key'); })() let same_global_scoped_symbol = Symbol.for("key"); //retrieves symbol with key
This is because when Symbol.for is used, if no value has been previously created with the given key, it is created and indexed in the global symbol registry. If a value has been created previously, the value is thus retrieved from the registry and returned.
In-built Symbols
The ES6 specification defines some set of Symbols that are inbuilt. These Symbol values are defined and available by default. They are also known as Well known SymbolThese inbuilt Symbols serve as a form of language extension points, that allows developers to hook into inbuilt behaviour for their custom data structures.
They are like dunder methods (magic methods) in Python.
For example, JavaScript has a default string format when toString is called on its inbuilt data type:
(true).toString() // prints "true" ([1,2,3,4]).toString() // prints "1,2,3,4" ({name:"John Agbas", "age":20}).toString() // prints "[object Object]"
But what if you want to define a custom class and have its instance printed, what will the output be?
Let’s see:
class Rectangle { constructor(height, width) { this.height = height; this.width = width; } } (new Rectangle(10,20)).toString() // prints "[object Object]"
"[object Object]" is also printed when toString is called on instance of a custom class.
What if you want to modify this inbuilt behaviour? What if we want "[object Rectangle]" to be printed?
One way is to use the Symbol.toStringTag inbuilt symbol to specify the string value to be used for the default description of an object. So if you want to have "[object Rectangle]" printed, then the class definition needs to be modified to:
class Rectangle { constructor(height, width) { this.height = height; this.width = width; } [Symbol.toStringTag] = "Rectangle" } // Now creating an instance and calling toString, would print "[object Rectangle]" (new Rectangle(10,20)).toString() // prints "[object Rectangle]"
And this, is an example of how the inbuilt symbols can be used to hook into inbuilt language behaviour for custom data structures.
Symbol.iterator is part of the couple of inbuilt symbols available in JavaScript. It allows for hooking into the inbuilt for of iteration mechanism. This is why the Symbol.iterator comes into play when talking about iterables and iterators in JavaScript.
But how does it work? How does Symbol.iterator allow us to create custom data structures that can be iterated with for of syntax?
Before we get to that, we first need to look into the concepts of Iterators and Iterables. And this is exactly what the next post about.
I am writing a book: TypeScript Beyond The Basics. Sign up here to be notified when it is ready.
2 comments:
Great post!
I love it. I bumped into Symbols exactly because of creating an iterator and this set of articles is just perrfect!
Post a Comment