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); // symbol

Trying 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); // true

The 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 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

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.



Every Crazy Thing JavaScript Does

A captivating guide to the subtle caveats and lesser-known parts of JavaScript.

Every Crazy Thing JavaScript Does

Sign up and receive a free copy immediately.


Leave a Comment

Your email address will not be published. Required fields are marked *