Saturday, June 15, 2019

Symbols In JavaScript

This post is part of the series on Symbols, Iterables, Iterators and Generators. It focuses on Symbols in JavaScript.

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.

 [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
}

To make it more obvious that we have a computed key, we can defined the 'salutation' property with a function:

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 Symbol

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:

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

1 comment:

Anonymous said...

Great post!