How to Use Well-Known Symbols in JavaScript

Last updated on October 15, 2022
How to Use Well-Known Symbols in JavaScript

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 on objects of a particular class.

Let's explore some of these well-known symbols.

11 Amazing New Features in ES13

11 Amazing New Features in ES13
Get up to speed with all the latest features added in ECMAScript 13 to modernize your JavaScript with shorter and more expressive code.

Symbol.hasInstance

The Symbol.hasInstance method customizes the behavior of the instanceof operator. Generally, when you use the instanceof operator like this:

obj instanceof type

JavaScript 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

11 Amazing New Features in ES13

11 Amazing New Features in ES13
Get up to speed with all the latest features added in ECMAScript 13 to modernize your JavaScript with shorter and more expressive code.

See also