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.
11 Amazing New JavaScript Features in ES13
This guide will bring you up to speed with all the latest features added in ECMAScript 13. These powerful new features will modernize your JavaScript with shorter and more expressive code.