DotNetCurry Logo

Meta Programming in ES6 using Symbols

Posted by: Ravi Kiran , on 3/21/2016, in Category HTML5 & JavaScript
Views: 10344
Abstract: Symbols is a new feature in ES6 that makes meta programming possible in JavaScript. This article explains what Symbols are and how to use them.

While writing an application of considerable size in any language, at some point, we need a way to store certain intermediary data in a special property or, a special object. This special property or object shouldn’t be easily accessible to the rest of the application to avoid any accidental reassignment of a value. The most common way in use in JavaScript applications is by creating a string with current timestamp attached to it. Though we are able to create a unique property name in this way, the data is not secured. The property is easily enumerable. So someone can easily read the value and change it.

This article is published from the DNC Magazine for Developers and Architects. Download this magazine from here [Zip PDF] or Subscribe to this magazine for FREE and download all previous and current editions

What is a Symbol and how to create one?

ECMAScript 2015 (ES2015 or, ES6) solves this problem through Symbols. Symbol is a new type added to ECMAScript 6 (ECMAScript 2015). It is used to generate a new value on every instance. Value of a Symbol is not shown to the outside world, but the type makes sure that the value is unique. The Symbol object can be used as name of a property or name of a method on an object or a class.

A Symbol can be created using the Symbol factory function. The syntax is shown below:

var s = Symbol();

It is important to remember that Symbol() is a function and not a constructor. An attempt to call it as a constructor would result in an error.

var s = new Symbol()  //error

If you try to print value of a Symbol object on the console, you will not see the actual value stored internally, you will see just a Symbol.

symbols

Every call to the Symbol factory function produces a new Symbol. An attempt to compare any pair of symbols results in a Boolean false value.

var s1 = Symbol();
var s2 = Symbol();

console.log(s1 === s2);  //false

It is possible to create named symbols. We can pass a name as a string parameter to the Symbol factory function.

var s = Symbol("mySymbol");

But if we use the factory function to create two different Symbols, the two different symbols that will be created, won’t be the same.

var s1 = Symbol("mySymbol");
var s2 = Symbol("mySymbol");

console.log(s1 === s2);  //false

The only way to create a Symbol that can be referred again is using the Symbol.for method. The first call to this method creates and returns a new Symbol with the name provided. Subsequent calls with the same name returns the Symbol that exists in the memory.

var s1 = Symbol.for("mySymbol");
var s2 = Symbol.for("mySymbol");

console.log(s1 === s2);  //true

When a named Symbol is printed on the console, it displays Symbol(name-of-the-symbol).

Named Symbol

Using Symbols

As already mentioned, Symbols can be used to create properties and methods in objects and classes. We can do the following with Symbols:

 var prop = Symbol();
var method = Symbol();

var obj = {
    [prop]:"Ravi",
    [method]: function(){
        console.log("I am a symbol function!!");
    }
};

If you create the Symbols inline while creating properties and methods, you will lose their references. Getting exact references of the Symbols is almost impossible. As we have references stored in variables, we can access the members in the object using them as follows:

console.log(obj[prop]); //Prints value of the property
obj[method]();  //Calls the method

Similarly, we can use the above Symbols while creating a class. Following example shows this:

var prop = Symbol();
var method = Symbol();

class Sample{
    constructor(){
        this[prop] = "some random value";
    }

    [method](){
        console.log("Method is called....");
    }
}

var obj = new Sample();
console.log(obj[prop]);
console.log(obj[method]());

Following are the characteristics of Symbol based members in an object:

· They are not iterable. Meaning, they are not accessible in for…in loop over properties of the object

· They are not serialized when the object is converted to JSON

· They aren’t included in the list of properties returned by Object.getOwnProperties method

Because of this, Symbols are almost private properties in objects. We can still access values of the Symbol properties. We will discuss this in the next section.

Accessing Symbols in an object

There is no direct way to get reference of a Symbol property if we don’t already have one. Objects of all Symbol properties can be obtained using the Object.getOwnPropertySymbols method. Following snippet gets them and prints the symbols. As you would have already guessed, the output of the iteration won’t make much sense unless the Symbols have names.

accessing-es6-symbols

As we can see, the result on the console displays only one Symbol, it is Symbol of the property. Symbol property of the method is not displayed here.

Using these Symbols, one can iterate over the list and read values of the properties, but meaning of the value won't be known unless the Symbols have meaningful names. So the data stored in the Symbol properties is still private in a way, as the values are not directly exposed and are difficult to understand even if someone accesses them.

Well-known Symbols

ES2015 has a list of predefined Symbols that are used by the JavaScript language for different purposes. They can be used to control the behavior of objects that we create. In this section, we will get to know the available Symbols in ES2015 and how to use them.

Note: Not all of the following APIs work in all environments that currently support ES2015 to some extent. Check the ES6 compatibility table before testing the feature.

1. Symbol.hasInstance: It is a method that can be used to customize the way instanceof operator works with a type. By default, instanceof checks if the object is an instance of the type referred and returns false if it doesn’t satisfy the condition. We can customize this behavior using Symbol.hasInstance. Following is an example of this:

class Person{
    constructor(){
        this.name = "Ravi";
        this.city = "Hyderabad";
    }

    [Symbol.hasInstance](value){
        if(value && typeof(value) !== "object" && "name" in value && "city" in value)
            return true;

        return false;
    }
}

var p = new Person();

var p2 = {name: "Ram", city: "Chennai"};

console.log(p instanceof Person);
console.log(p2 instanceof Person);

Both the console.log statements in the above code block would print true.

 

2. Symbol.toPrimitive: This method can be used to customize the way an object gets converted to a primitive type. JavaScript tries to convert an object to a primitive type when the object is used in an operation with a value of a primitive type.

Some of the common cases where JavaScript tries to convert reference types to primitive types are when operations like addition with a number or, concatenation with a string are performed. If the object is not compatible, the language generates either an error or, produces strange results. The Symbol.toPrimitive method can be used to avoid such cases and convert the object to a type based on the value it is operated with.

var city = {
    name:"Hyderabad",
    temperature: 40,
    [Symbol.toPrimitive](hint){
        console.log(hint);
        switch(hint){
            case "number": return this.temperature;
            case "string": return this.name;
            default: return this;
        }
    }
};

console.log(city*2);
console.log("City is ".concat(city));

3. Symbol.toStringTag: This method is used to customize the way toString method behaves in an object. A call to toString on an object results either in name of the type of which the object is an instance, or just Object if the object is created without using any type. The toStringTag method can be used to change the way the method works. Following example shows a usage of it:

var city = {
    name: 'Hyderabad',
currentTemperature: '40'
};

console.log("" + city); // '[object Object]'

city[Symbol.toStringTag] = function(){
    return this.name;
};

console.log("" + city); // '[object Hyderabad]'

4. Symbol.match, Symbol.replace, Symbol.search and Symbol.split:

The regular expression methods of String forward the calls to the corresponding method on their regular expression parameter. The actual implementation of these methods is in the regular RegExp class and names of the methods are values of these Symbols.

· String.prototype.match(regExpToMatch) calls regExpToMatch[Symbol.match](this)

· String.prototype.replace(searchPattern, replaceValue) calls searchPattern[Symbol.match](this, replaceValue)

· String.prototype.search(searchPattern) calls searchPattern[Symbol.search](this)

· String.prototype.split(separatorPattern, limit) calls separatorPattern[Symbol.split](this, limit)

5. Symbol.unscopables: Unlike other Symbols, it is an object that is used to hide certain members of an object inside a with block.

The with operator treats members of an object as variables inside the block. It is not allowed to be used in a strict mode. By default, all members of the object are accessible inside the with block.

Symbol.unscopable is used by Array to hide a few of the methods. If you print Array.prototype[Symbol.unscopable] in the console, you will see the following:

unscopable-symbol

6. Symbol.species: Symbol.species is used to customize the way instances are created in certain circumstances on a type. At times, a class may need to create an instance of itself in a method defined in it. By default, the method creates the instance of the same class. Imagine a scenario where the method is called using an instance of a child class. In such a case, it would be more appropriate to create instance of the child class rather than the current class (which is the parent).

This pattern is used in the Array.prototype.map function. It checks for existence of the Symbol.species method in the instance used to call the function. If it doesn’t exist, it creates instance of the type of the object. Otherwise, it creates instance of the type that is returned by the species function.

Following example shows how Symbol.species can be used:

class Building{
    constructor(){
        this.floors = 2;
        this.hasGarden = true;
    }

    clone(){
        var hasSpecies = this.constructor[Symbol.species];
        var cloneObject = hasSpecies? new this.constructor[Symbol.species]() : new this.constructor();

        cloneObject.floors = this.floors;
        cloneObject.hasGarden = this.hasGarden;

        if(cloneObject instanceof Office){
            cloneObject.wingsPerFloor = this.wingsPerFloor;
        }

        return cloneObject;
    }
}

class House extends Building{
    constructor(){
        super();
        this.rooms = 3;
    }

    static get [Symbol.species]() { return Building; }
}
class Office extends Building{
    constructor(){
        super();
        this.wingsPerFloor;

        this.wingsPerFloor = 2;
    }
}

var h = new House();
h.floors = 3;
console.log(h.clone());

var o = new Office();
console.log(o.clone());

In the above example, we have a parent class, Building. Two classes inherit from this class, House and Office. The clone method defined in the Building class creates a copy of the object and returns it. Out of the two classes, the House class has an implementation of the Symbol.species static member.

The clone method in the Building class creates a clone based on presence [T1] and return value of this method. The class House implements it and returns Building. So when the clone method is called with an object of House type, it returns an object of Building type. And when the clone method is called with an Office object, it returns an object of the same type.

7. Symbol.isConcatSpreadable: This is a property that can be used to configure behavior of the concat operation on arrays. When two arrays are concatenated, the second array is spread and its elements are added as individual items in the resultant array. By setting the property Symbol.isConcatSpreadable to false on the array, we can prevent spreading of the second array and the whole array will be added as an object entry in the resultant array.

let arr = [1,2];
let arr2 = arr.concat([3,4], 5);   //[1,2,3,4,5]

let arr = [1,2];
arr[Symbol.isConcatSpreadable] = false;
let arr2 = arr.concat([3,4], 5);   //[1,2,[3,4],5]

Conclusion

Symbols are added to JavaScript to make the job of creating unique properties easier and also to make a number of default options of the language customizable. They bring in the ability of meta programming to the language. Let’s use Symbols and other features of ES6 to make our future JavaScript applications shine.

Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+
Further Reading - Articles You May Like!
Author
Rabi Kiran (a.k.a. Ravi Kiran) is a developer working on Microsoft Technologies at Hyderabad. These days, he is spending his time on JavaScript frameworks like AngularJS, latest updates to JavaScript in ES6 and ES7, Web Components, Node.js and also on several Microsoft technologies including ASP.NET 5, SignalR and C#. He is an active blogger, an author at SitePoint and at DotNetCurry. He is rewarded with Microsoft MVP (Visual Studio and Dev Tools) and DZone MVB awards for his contribution to the community


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!