The Complete Guide to JavaScript Symbols
In this article, we’re going to learn all about JavaScript symbols and how to use them in practice.
Creating symbols
The Symbol is a data type that was added to JavaScript in ES6. To create a symbol, you can use the global Symbol() function.
const sym1 = Symbol();The Symbol() function accepts an optional string that serves as the description of the symbol.
const sym2 = Symbol('color');
const sym3 = Symbol('maxSpeed');
const sym4 = Symbol('age');
You can access this description with the description property of the Symbol. If no description was set, this property would be undefined.
console.log(sym1.description); // undefined
console.log(sym2.description); // color
console.log(sym3.description); // maxSpeed
console.log(sym4.description); // age
Every time you call the Symbol() function, it returns a new unique value.
console.log(Symbol() === Symbol()); // false
console.log(Symbol('color') === Symbol('color')); // false
console.log(Symbol('age') === Symbol('age')); // false
As a symbol is a primitive value, using the typeof operator on it will return a symbol string.
console.log(typeof sym1); // symbolTrying to use the new operator to create a symbol will cause a TypeError. This prevents the creation of an explicit Symbol wrapper object in place of a new symbol value.
const num1 = Number(2);
console.log(typeof num1); // number
const num2 = new Number();
console.log(typeof num2); // object
const bool1 = Boolean(true);
console.log(typeof bool1); // boolean
const bool2 = new Boolean(true);
console.log(typeof bool2); // object
const sym1 = Symbol('color');
console.log(typeof sym1); // symbol
// β TypeError
const sym2 = new Symbol('color');
console.log(typeof sym2);
Sharing symbols
ES6 comes with a global symbol registry that allows you to share symbols globally. To create a symbol for sharing, you use the Symbol.for() method instead of the Symbol() function. Symbol.for() takes a key and returns a symbol value from the registry.
For example:
const color = Symbol.for('color');The Symbol.for() method first searches the global symbol registry for a symbol with the color key and returns it if it exists. Otherwise, Symbol.for() creates a new symbol, adds it to the registry with the specified color key, and returns the symbol.
Afterward, calling Symbol.for() with the same key returns this same symbol, as it now exists in the registry.
const carColor = Symbol.for('color');
console.log(color === carColor); // trueThe Symbol.keyFor() method works together with Symbol.for() to retrieve information from the global symbol registry. You can get the key of a symbol that exists in the registry with Symbol.keyFor().
console.log(Symbol.keyFor(carColor)); // 'color'If the key doesn’t exist, Symbol.keyFor() returns undefined.
const filename = Symbol('filename');
console.log(Symbol.keyFor(filename)); // undefined;
Uses of symbols
1. Enumerations
With symbols, we can define a group of constants to set a variable to a predefined set of values – an enumeration.
Let’s say we want to create a variable to store a value representing the day of the week. To create an enum, we could use strings like ‘Mon', 'Tue', 'Wed', etc.
const daysOfWeek = {
  Mon: 'Mon',
  Tue: 'Tue',
  Wed: 'Wed',
  Thu: 'Thu',
  Fri: 'Fri',
  Sat: 'Sat',
  Sun: 'Sun',
};
const dayOfWeek = daysOfWeek.Tue;
By using symbols in place of the string, we can ensure that the constants are always distinct from one another, and not worry about duplicate string values.
const daysOfWeek = {
  Mon: Symbol('Mon'),
  Tue: Symbol('Tue'),
  Wed: Symbol('Wed'),
  Thu: Symbol('Thu'),
  Fri: Symbol('Fri'),
  Sat: Symbol('Sat'),
  Sun: Symbol('Sun'),
};
const dayOfWeek = daysOfWeek.Tue;
2. Prevention of property name clashes
Since a symbol is always guaranteed to be unique, we can use it to avoid name clashes when entirely separate sections of code need to store data on an object. The different sections may not be aware of each other, but each wants to make sure that the property it uses is not going to be used by another.
For example:
// module-1.js
const module1 = () => {
  // Prevent external access with closure
  const module1Sym = Symbol();
  return (obj) => {
    // Put some data on obj that this module can access later
    obj[module1Sym] = 'module 1 data';
  };
};
// module-2.js
const module2 = () => {
  // Prevent external access with closure
  const module2Sym = Symbol();
  return (obj) => {
    // Put some data on obj that this module can access later
    obj[module2Sym] = 'module 2 data';
  };
};
// index.js
const obj = {};
module1(obj);
module2(obj);
If the modules each used a string instead of a symbol for the property name, it could cause problems as the names could clash.
For example:
// module-1.js
const module1 = () => {
  return (obj) => {
    // Put some data on obj that this module can access later
    obj.moduleData = 'module 1 data';
  };
};
// module-2.js
const module2 = () => {
  return (obj) => {
    // Put some data on obj that this module can access later
    obj.moduleData = 'module 2 data';
  };
};
// index.js
const obj = {};
module1(obj);
module2(obj);
// 'module 1 data' has been overwritten by the call the module2()
Note that symbol properties are not enumerable, if you call Object.keys() on the object, they will not be included in the array result.
const dayOfWeek = Symbol('dayOfWeek');
const event = {
  [dayOfWeek]: daysOfWeek.Tue,
  description: 'Birthday',
};
console.log(Object.keys(event)); // [ 'description' ]Object.getOwnPropertyNames() returns an array of all the enumerable and non-enumerable properties of an object, but symbol properties are still excluded.
console.log(Object.getOwnPropertyNames(event)); // [ 'description' ]To get all the symbol properties, you use the Object.getOwnPropertySymbols() method. This method returns an array of all symbol properties found directly on a given object.
console.log(Object.getOwnPropertySymbols(event)); // [ Symbol(dayOfWeek) ]
Well-known symbols
The Symbol class comes with static properties known as well-known symbols. They are used to implement and customize the behavior of certain built-in JavaScript operations.
Let’s explore some of these well-known symbols:
Symbol.hasInstance
The Symbol.hasInstance method customizes the behavior of the instanceof operator. Generally, when you use the instanceof operator like this:
obj instanceof typeJavaScript will call the Symbol.instanceof method like this:
type[Symbol.hasInstance](obj);Here’s an example where we use the instanceof method on an instance of a user-defined class.
class List {}
console.log([] instanceof List); // false
Going by the default behavior of instanceof, [] is an Array, not an instance of the List class, so instanceof returns false here.
If we want to change this behavior and make instanceof return true in this scenario, we would customize the Symbol.hasInstance method like this:
class List {
  static [Symbol.hasInstance](obj) {
    return Array.isArray(obj);
  }
}
console.log([] instanceof List); // true
Symbol.iterator
With the Symbol.iterator method, we can specify if and how objects of a class can be iterated over. When this method is present, we will be able to use the for...of loop and spread syntax (...) on the objects of the class.
When you use the for...of loop on an array:
const numbers = [1, 2, 3];
for (const num of numbers) {
  console.log(num);
}
/*
1
2
3
*/
Internally, JavaScript first calls the Symbol.iterator method of the numbers array to get the iterator object. Then it continuously calls the next() method on the iterator object and copies the value property in the num variable, It exits the loop when the done property of the iterator object is true.
var iterator = numbers[Symbol.iterator]();
console.log(iterator.next()); // Object {value: 1, done: false}
console.log(iterator.next()); // Object {value: 2, done: false}
console.log(iterator.next()); // Object {value: 3, done: false}
console.log(iterator.next()); // Object {value: undefined, done: true}
By default, objects of a user-defined class are not iterable. But we can make them iterable with the Symbol.iterator method, as you’ll use in the following example:
class List {
  elements = [];
  add(element) {
    this.elements.push(element);
    return this;
  }
  // Generator
  *[Symbol.iterator]() {
    for (const element of this.elements) {
      yield element;
    }
  }
}
const colors = new List();
colors.add('red').add('blue').add('yellow');
// Works because of Symbol.iterator
for (const color of colors) {
  console.log(color);
}
/*
red
blue
yellow
*/
console.log([...colors]);
// [ 'red', 'blue', 'yellow' ]
Symbol.toStringTag
This symbol lets us customize the default string description of the object. It is used internally by the Object.prototype.toString() method.
class CarClass {
  constructor(color, maxSpeed, age) {
    this.color = color;
    this.maxSpeed = maxSpeed;
    this.age = age;
  }
}
const car = new CarClass('red', 100, 2);
console.log(Object.prototype.toString.call(car));
// [object Object]
Here is the default implementation of Symbol.toStringTag was outputted. Here’s how we can customize it:
class CarClass {
  constructor(color, maxSpeed, age) {
    this.color = color;
    this.maxSpeed = maxSpeed;
    this.age = age;
  }
  get [Symbol.toStringTag]() {
    return 'Car';
  }
}
const car = new CarClass('red', 100, 2);
console.log(Object.prototype.toString.call(car));
// [object Car]
Symbol.toPrimitive
The Symbol.toPrimitive method makes it possible for an object to be converted to a primitive value. It takes a hint argument that specifies the type of the resulting primitive value. This hint argument can be one of 'number', 'string', or 'default'.
Here’s an example of using the Symbol.toPrimitive method.
class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }
  [Symbol.toPrimitive](hint) {
    if (hint === 'string') {
      return `${this.amount} ${this.currency}`;
    } else if (hint === 'number') {
      return this.amount;
    } else if (hint === 'default') {
      return `${this.amount} ${this.currency}`;
    }
  }
}
const price = new Money(500, 'USD');
console.log(String(price)); // 500 USD
console.log(+price); // 500
console.log('Price is ' + price); // Price is 500 USD
Conclusion
In this article, we learned how to create JavaScript symbols and share them in the global symbol registry. We saw the advantages of using symbols, and how well-known symbols can be used to customize the behavior of built-in JavaScript operations.

 
					
















