Symbol—A New Primitive Type

JavaScript previously had five primitive types: number, string, boolean, null, and, undefined; now it has one more. Symbol is a new primitive type in JavaScript intended for limited specialized use. Symbols can be used for three distinct purposes:

  • To define properties for objects in such a way they don’t appear during normal iteration—these properties are not private; they’re just not easily discovered like other properties.

  • To easily define a global registry or dictionary of objects.

  • To define some special well-known methods in objects; this feature, which fills the void of interfaces, is arguably one of the most important purposes of Symbol.

Interfaces in languages like Java and C# are useful for design by contract and serve as a specification or a listing of abstract functions. When a function expects an interface it is guaranteed, in these languages, that the object passed will conform to the specifications of the interface. There’s no such capability in JavaScript, however. We’ll see how Symbol helps fill the gap.

Let’s explore each of the benefits with examples.

Hidden Properties

Until Symbol was added to JavaScript, all properties of an object were visible when iterated using forin. Symbol changes that behavior; a Symbol property is not visible during such iteration.

If a property is intended to be visible during normal iteration, then define it as usual. However, if you like for a property to store some special data, like metadata about an object, that should not be visible using normal iteration, then you may hide it as a Symbol property.

Let’s take a look at the behavior of a Symbol property using an example.

 const​ age = Symbol(​'ageValue'​);
 const​ email = ​'emailValue'​;
 
 const​ sam = {
  first: ​'Sam'​,
  [email]: ​'[email protected]'​,
  [age]: 2
 };

age is defined as a Symbol using the Symbol() function. The argument passed to this function has no real use other than for debugging purposes. A Symbol created using this function is unique and distinct from any other Symbol created using the function. Symbols can’t be created using the new operator.

email is defined as a string. Within the sam object, first is a property and it is assigned the string value ’Sam’ representing the name. Had we defined the next property as email: ... then the property name will be email. But our intention is to define the property name as emailValue, which is held inside the variable named email. Thus, we wrap the variable named with []. In effect, [email] defines a property named emailValue. Likewise, we define a third property with [age], but unlike email, which refers to a string, age refers to a Symbol. Thus we defined the third property where the property itself is of Symbol type while the value held by that property is of type number.

Let’s iterate over the properties of the instance sam next.

 console.log(​'iterating over properties:'​);
 for​(​const​ property ​in​ sam) {
  console.log(property + ​' : '​ + sam[property]);
 }
 
 console.log(​'list of property names:'​);
 console.log(Object.getOwnPropertyNames(sam));

First we iterate over the properties of the instance sam using forin and print both the property names and the corresponding values. Next we query for all the property names on the object using Object’s getOwnPropertyNames() method. The result for these two actions is shown next:

 iterating over properties:
 first : Sam
 emailValue : [email protected]
 list of property names:
 [ 'first', 'emailValue' ]

Both the properties first and emailValue are displayed, but the property ageValue, which is of type Symbol, is not exposed.

A Symbol property is hidden from normal iteration. However, it is not private or encapsulated. Any code with access to the object can both access and change the value for a Symbol property. Also, Object’s getOwnPropertySymbols() method will give a list of all the Symbol properties, like so:

 console.log(​'list of symbol properties'​);
 console.log(Object.getOwnPropertySymbols(sam));
 
 console.log(​'accessing a symbol property:'​);
 console.log(sam[age]);
 
 console.log(​'changing value...'​);
 sam[age] = 3;
 console.log(sam[age]);

The getOwnPropertySymbols() method does not hold back any Symbol properties. If an object has no Symbol properties, the method will return an empty array. Otherwise, it returns an array of Symbols with one element for each Symbol property.

The syntax sam.age will try to access a property named age, which does not exist in the instance. Our intention is to access a property whose name is held within the age variable. To achieve this, we use the [] syntax, like so: sam[age]. Likewise, to set the value of the property whose name is held within the age variable, we place sam[age] on the left-hand side of the assignment expression. The output of the code shows the list of Symbol properties and the result of our efforts to access/change the value of the ageValue Symbol property:

 list of symbol properties
 [ Symbol(ageValue) ]
 accessing a symbol property:
 2
 changing value...
 3

We played with our own instance sam in the previous example. Next, let’s examine a built-in object for Symbols.

 const​ regex = ​/cool/​;
 
 process.stdout.write(​'regex is of type RegExp: '​);
 console.log(regex ​instanceof​ RegExp);
 
 process.stdout.write(​'Properties of regex: '​);
 console.log(Object.getOwnPropertyNames(regex));
 
 process.stdout.write(​'Symbol properties of regex: '​);
 console.log(Object.getOwnPropertySymbols(regex));
 
 console.log(​"Symbol properties of regex's prototype: "​);
 console.log(Object.getOwnPropertySymbols(Object.getPrototypeOf(regex)));

As an aside, in the code, in addition to using console.log() we’re using process.stdout.write(). The log() function produces a new line. In this example, to stay on the same line after printing output, we use the write() method that’s available in node.js. The variable regex holds a reference to an instance of the RegExp regular expression class. We first confirm that the instance is of the type we expect it to be. Then we query for all its properties using the getOwnPropertyNames() method. Then we query for all its Symbol properties. Finally we perform the query on the instance’s prototype, accessed through the Object.getPrototypeOf() method, which returns the same instance as RegExp.prototype.

The output from the code is shown next:

 regex is of type RegExp: true
 Properties of regex: [ 'lastIndex' ]
 Symbol properties of regex: []
 Symbol properties of regex's prototype:
 [ Symbol(Symbol.match),
  Symbol(Symbol.replace),
  Symbol(Symbol.search),
  Symbol(Symbol.split) ]

The little experiment we ran reveals that RegExp has a handful of Symbol properties. These properties are actually special methods. We’ll soon see how we can benefit from special methods for our own classes. Before that, let’s explore the uniqueness property of Symbol.

Global Registry with Symbol

When we create a Symbol using the Symbol() function, the argument passed to it has no significance and each call to Symbol() creates a unique Symbol. Let’s quickly verify this behavior with an example.

 const​ name = ​'Tom'​;
 const​ tom = Symbol(name);
 const​ jerry = Symbol(​'Jerry'​);
 const​ anotherTom = Symbol(name);
 
 console.log(tom);
 console.log(​typeof​(tom));
 console.log(tom === jerry);
 console.log(tom === anotherTom);

We created three Symbols. Two of those were created using the same argument name. However, since the arguments passed to the function have no significance and the Symbol created by each call is unique, we can see in the output that the Symbol instances are all unequal:

 Symbol(Tom)
 symbol
 false
 false

The aforementioned behavior changes a bit when the Symbol.for() method is used to create a Symbol instead of the Symbol() function. The for() method takes a key as argument, creates a Symbol if one already does not exist for that key in a global registry, and returns either the newly created instance or the preexisting one. At any time we may obtain the pre-created Symbol for a given key using the keyFor() method. Let’s explore these two methods with an example.

 const​ masterWizard = Symbol.​for​(​'Dumbledore'​);
 const​ topWizard = Symbol.​for​(​'Dumbledore'​);
 
 console.log(​typeof​(masterWizard));
 console.log(masterWizard);
 console.log(masterWizard === topWizard);
 
 console.log(​'Dumbledore'​ === Symbol.keyFor(topWizard));

We first create a Symbol using the for() method, passing an argument to it, and assign the result to the variable masterWizard. We repeat this step, using the same argument for the for() method, but assign the result this time to the variable named topWizard. In the last line of the code we invoke the keyFor() method, passing to it the second Symbol we created. Unlike the Symbol() function, the argument passed to for() has significance—it represents a unique key for Symbol that is being created or fetched from the global registry. In this example, the first call to for() creates a new Symbol instance whereas the second call to for() fetches the Symbol created by the first call, since the argument is the same as in the first call. The call to keyFor() returns the key associated with the Symbol in the registry. We can verify the code’s behavior from the output:

 symbol
 Symbol(Dumbledore)
 true
 true

This feature of uniqueness of Symbol is used in JavaScript to define special well-known functions, as we’ll see next.

Special Well-Known Symbols

In languages like Java and C# we expect classes to collaborate with each other through interfaces. For example, if a class expects another class to have a compare() method, it would expect that class to implement a Comparator interface. JavaScript does not follow such traditions or ceremony. The contracts are rather informal and relaxed. If a class expects another class to have a method, it simply expects to find that method—as simple as that.

While there is merit to that simplicity, from the documentation point of view a single source of truth is still useful. Furthermore, not having a clear way to specify that you expect a class to implement a particular method or a property can lead to errors.

Suppose you expect a programmer using your library to create a class with a special method named myWonderfulMethod. It’s hard to track if a programmer makes a typo and creates a method with the name myWonderfulmethod. Also, since the name myWonderfulMethod is not a standard name, a class may have already implemented that method for some other purpose than what you expected. A lack of a clear way to uniquely specify a method or property name can lead to errors and confusion. This is another place where Symbol comes to rescue.

Since a Symbol is unique, instead of expecting a class to implement a method named myWonderfulMethod, if you expect it to implement a special method [Symbol.for(’myappname.myWonderfulMethod’)] then there’s no ambiguity.

JavaScript has nearly a dozen well-known Symbols, like Symbol.iterator, Symbol.match, Symbol.replace, and Symbol.search, to mention a few. Some functions and methods expect classes to implement methods with one or more of these well-known Symbol names in order to pass instances of those classes as arguments.

One example of a function that depends on a special well-known Symbol is String’s search() method. If the argument given to search is not an instance of RegExp, it then creates a RegExp using the given argument as the constructor argument. However, that’s true only if the given argument to search() does not support the special method named Symbol.search. If that method is available on the instance, then that method is used to perform the search. Let’s create a class with this special method to learn about this behavior.

 class​ SuperHero {
 constructor​(name, realName) {
 this​.name = name;
 this​.realName = realName;
  }
 
  toString() { ​return​ ​this​.name; }
 
  [Symbol.search](value) {
  console.info(​'this: '​ + ​this​ + ​', value: '​ + value);
 return​ value.search(​this​.realName);
  }
 }

We created a class named SuperHero using the new class syntax. It’s much like what you may be used to in languages like Java and C#. We will explore the class syntax in Chapter 7, Working with Classes.

An instance of the class SuperHero holds two fields: name and realName. The Symbol.search() method takes in a value as parameter and searches it for the contents present in the realName field. In addition, the method prints an informational message about the current context object this and the passed-in value argument.

Let’s now make use of this class to see the power of the special search() method:

 const​ superHeroes = [
 new​ SuperHero(​'Superman'​, ​'Clark Kent'​),
 new​ SuperHero(​'Batman'​, ​'Bruce Wayne'​),
 new​ SuperHero(​'Ironman'​, ​'Tony Stark'​),
 new​ SuperHero(​'Spiderman'​, ​'Peter Parker'​) ];
 
 const​ names = ​'Peter Parker, Clark Kent, Bruce Wayne'​;
 for​(​const​ superHero ​of​ superHeroes) {
  console.log(​`Result of search: ​${names.search(superHero)}​`​);
 }

The code creates an array of SuperHero instances. Finally, it loops through the instances and invokes the search on a names variable, passing in the instance at hand.

The output from the code shows that the specially defined method in the class SuperHero is called during the call to the search() method on names.

 this: Superman, value: Peter Parker, Clark Kent, Bruce Wayne
 Result of search: 14
 this: Batman, value: Peter Parker, Clark Kent, Bruce Wayne
 Result of search: 26
 this: Ironman, value: Peter Parker, Clark Kent, Bruce Wayne
 Result of search: -1
 this: Spiderman, value: Peter Parker, Clark Kent, Bruce Wayne
 Result of search: 0

Each of the special well-known Symbols serves as a special method in places where its presence is expected. For a complete list of the well-known Symbols and their purpose, refer to the appropriate section in the ECMAScript 2015 Language Specification.[15]

We discussed the three benefits that Symbol provides. One of the most common Symbols in JavaScript is the well-known Symbol.iterator. You will learn to use it for creating custom iterators next.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset