What Are the Advantages of Using Symbols ?
Problem
You want to know why and how to use symbols in a project.
Solution
Symbols give you the ability to create unique tokens that will never clash with other symbols. Similar to UUIDs (Universal Unique Identifiers)
, symbols can be used on objects to make unique properties.
The Code
let mySym1 = Symbol('This is my first Symbol');
let mySym2 = Symbol(); //returns false
console.log(mySym1 == mySym2);
console.log(mySym1.toString()) //returns Symbol(This is my first Symbol)
Listing 15-1.
Creating Symbols
How It Works
Symbols are unique objects that can be used as properties of an object. They are primitive datatypes, similar to Number or String. When creating a symbol, the syntax is similar to creating an object instance. The main difference is that you do this without the new operator. Using new when creating an object would result in a TypeError.
When creating a symbol, you can also create a label for that symbol. A label does not change the value of the symbol; it is mostly for debugging. To create a label, pass a string as the first parameter of the symbol constructor. If you wanted to see the label, for example in the browser’s console
, use the toString method. Symbols with the same label are not equal to each other.
How Do You Create a Symbol?
Problem
You want to create a symbol to use as an object property.
Solution
Creating symbols is very similar to creating an object instance. One exception is you should not use the new operator. This would throw an TypeError. Each call of the Symbol function generates a unique value. Labels can be added to symbols for debugging purposes. These labels do not change the value generated by a symbol. Using bracket notation, you can use symbols as properties of an object, thereby creating unique values for each property.
The Code
flet characterObj = {};
let dad = Symbol();
characterObj.name ='Elliot'
characterObj[dad] = 'Mr. Robot';
console.log(Object.keys(characterObj));
Listing 15-2.
Creating Symbols to Use as Object Properties
How It Works
Once a symbol has been created, you can use bracket notation to make it the property of an object. Because you are using a symbol, the property has a unique value that cannot clash with another symbol. The symbol can then be given a value. The unique values of symbols make them different from using strings or numbers as property keys.
Can You Access a Symbol by its String Name?
Problem
The symbol global registry contains many symbols. How do you access the one you want?
Solution
Using the Symbol.for() method will allow you to access a symbol from the registry. If the symbol does not exist, one will be created.
The Code
let player1 = Symbol.for('player1'); //creates the symbol and puts it in the registry
console.log(Symbol.for('player1') == player1); //returns true
Listing 15-3.
Accessing a Symbol Using the Symbol.for() Method
How It Works
The global symbol registry constrains multiple symbols. It is very similar to having multiple global objects. Using the Symbol.for() method allows you to retrieve a symbol if it currently exists. If the symbol is not currently in the registry, the symbol will be created. Moving forward every time there is a request for that symbol the same one will be returned. Symbols in the registry can be accessed from different realms. For example, Iframes and service workers can share symbols.
Can You Return a Key Based on a Symbol?
Problem
You want to return a key based on a symbol.
Solution
The
Symbol.keyFor() method
returns a key based on its symbol.
The Code
let firstPlayer = Symbol.for('player1'); //creates the symbol and puts it in the registry
console.log(Symbol.keyFor(firstPlayer)) //returns player1
Listing 15-4.
Using the forKey() Method You Can Return the Key of a Symbol
How It Works
When using the keyFor() method, it will return the key for any given symbol that is in the registry. If this key does not exist, the method will return undefined.
Can You Use a Symbol as Part of a Function or Method in an Object?
Problem
You want to know if symbols can be used to define functions.
Solution
Because of computed property names
, symbols can be used as part of a function definition.
The Code
let helloSymbol = Symbol('Hello World Function');
let myObj = {
[helloSymbol] () {
return 'Hello World';
}
}
console.log(myObj[helloSymbol]());
let iterableObj = {
Symbol.iterator]() {
let dataArray = ['this', 'that', 'other'];
let currentIndex = 0;
return {
next(){
if(currentIndex < dataArray.length){
return {value: dataArray[currentIndex++]};
}else{
return {done: true};
}
}
}
}
}
for(let x of iterableObj){
console.log(x); //returns this, that, other
}
Listing 15-5.
Computed Property Names Enable You to Use Symbols as Part of a Function Definition
How It Works
Computed property names
is a ES6 feature that allows you to create a property for an object literal or class using bracket ([ ]) notation. By first creating a public symbol, then using it as a property, this property will always be unique.
Another example is if the symbol was an iterator. This would make the object iteratable. In the second example the object has properties stored in an array. Using Chapter 13 as a reference on iterators, we can use the next function to continue to move to the next property until currentIndex has the same value as the length of the array. Then use a for-of loop to iterate through the object.
How Can a Symbol Override the Constructor Function of Your Custom Class ?
Problem
You have a situation where you want to return the constructor from a derived objet and not the current class.
Solution
The
Symbol.species property
will return the constructor function of the main class and not a subclass.
The Code
class ArraySubClass extends Array{
static get [Symbol.species]() {return Array;}
}
var subClassInstance = new ArraySubClass(1,2,3,4,5,6);
var derivedObj = subClassInstance.filter(function(value){
if(value % 2){
return value
}
});
console.log(derivedObj) //returns 1,3,5
console.log(derivedObj instanceof Array); //returns true
console.log(derivedObj instanceof ArraySubClass); //returns false
Listing 15-6.
The Property Species Used on the Symbol Object Returns the Constructor of a Derived Object
How It Works
To fully understand what Symbol.species does, you must first understand what a derived object is.
Derived objects
are created when an operation is called on the original object. In our example, ArraySubClass extends Array. Because of this ArraySubClass on its own it does not have a filter() method. By calling the filter() method, the original Array object is used to fulfill the request.
The object that returns as the result of the filter has a constructor
of the parent Array object and not that of the ArraySubClass. Using the instanceof operator shows that the derived object does not have the same constructor of the ArraySubClass but that of the Array object.
Can a Constructor Object Recognize an Object as its Instance?
Problem
You need to know if an object is an instance of a certain type.
Solution
Creating a custom instanceof operator with the hasInstance property will allow you to check for type
.
The Code
class CheckArrayInstance {
static [Symbol.hasInstance](instance){
return Array.isArray(instance)
}
}
var myArray = new Array();
console.log(myArray instanceof CheckArrayInstance); //returns true
Listing 15-7.
The hasInstance Property Will Check if the Constructor Object Recognizes Another Object as Instance
How It Works
Usually when using the
instanceof operator
, you can check the prototype of an object. However, you can customize the instanceof operator to get more specific and check the type of an object.
In Listing 15-7, we create the static method that checks if the instance is an array. If true, the method returns a value of true.
Can You Convert an Object to a Primitive Value ?
Problem
You want to determine the primitive value of an object.
Solution
When used as a method of an object, Symbol.toPrimitive can convert an object to its primitive type.
The Code
var PrimitiveObj = {
[Symbol.toPrimitive](hint){
if(hint == 'number'){
return 100 ;
}else if (hint == 'string'){
return 'this is a string';
}else{
return 'this is the default;
}
}
}
console.log(+PrimitiveObj) //returns 100
console.log(`${PrimitiveObj}`) //returns this is a string
console.log(PrimitiveObj + ' ') //returns this is the default
Listing 15-8.
The toPrimitive Method Converts an Object to its Primitive Type
How It Works
The toPrimitive method lets you create a function that will turn an object into a primitive. This method takes one property called
hint
. This property can have only one of three values—string, number, or default. In Listing 15-8, we call the function three different times.
The first time the plus (+) operator is used. Using this operator performs type conversion
. Using this operator, the object will be converted into a number. Under normal circumstances, the result would be not a number (NaN). Using the toPrimitive function allows us to see that the conversion to number was trying to take place. The result is that hint then has the value of number.
The second example uses a template literal (discussed in detail in Chapter 14). Since template literals use string literals, the value of hint would be string.
The third call would by default return an object when making the conversion. Because of our toPrimitive method, it returns default.
How Can You Get the Class of the Object You Are Working With?
Problem
You want to know the type of object you are working with and not have [object object] as a return value.
Solution
Adding the
toStringTag property
to a custom class will allow you to return the type of object you are working with.
The Code
var myDate = new Date();
var myArray = new Array();
var myObj = new Object();
class myClass{};
var myClassObj = new myClass();
console.log(Object.prototype.toString.call(myDate)); //returns [object Date]
console.log(Object.prototype.toString.call(myArray)); // returns [object Array]
console.log(Object.prototype.toString.call(myObj)); //returns [object object]
console.log(Object.prototype.toString.call(myClassObj)); //returns [object object]
class WithToStringTag{
get [Symbol.toStringTag](){
return 'WithToStringTag';
}
}
var withToStringTagObj = new WithToStringTag();
console.log(Object.prototype.toString.call(withToStringTagObj)); //returns [object WithToStringTag]
Listing 15-9.
The toStringTag Property Used in a Class Will Return the Type
How It Works
The built-in toString() method can be used in every object. This will allow you to get the class the object is based on. In order to access this method, we can use Object.prototype.toString.call. Using the call() method is important. Here it sets the value of this and has the result comes from the object that we are passing. For further understanding of the
call() method
, look at Chapter 12.
In Listing 15-9, all of the objects that are part of the JavaScript language return their class names. When you reach custom objects, the result is [object object]. By adding the Symbol.toStringTag method, you can return the name of the custom class and override the built-in toString method.
Can You Hide Properties of an Object?
Problem
You want certain properties not to show up when you’re doing a with statement.
Solution
The well-known Symbol.unscopables symbol will exclude property names from being exposed using a with statement.
The Code
class MyClass{
firstProp() {return 'First Prop';}
}
with(MyClass.prototype){
console.log(firstProp()) //returns First Prop
}
class MyClassWithUnscopables{
firstProp(){return 'First Prop';}
get[Symbol.unscopables](){
return {firstProp : true}
}
}
with(MyClassWithUnscopables.prototype){
console.log(firstProp()); //ReferenceError: firstProp is not defined
}
Listing 15-10.
Symbol.unscopables Will Prevent Properties from Being Exposed Using a with Statement
How It Works
The Mozilla Developer Network (MDN)
does not recommend using the with statement. However, when an expression is being used inside a with statement, methods could be called on that object. This brings us to
Symbol.unscopables
, which enables you to prevent certain properties from being exposed to the scope from inside the with statement.
Adding the unscopables symbol to the custom class protects the method from being executed within the with statement. In this instance, it returns a ReferenceError because it does not believe the method exists.