Before symbols were introduced in ES6 as a new type of primitive, JavaScript used seven main types of data, grouped into two categories:
Starting with ES6, symbols were added to the primitives group. Like all other primitives, they are immutable and have no methods of their own.
The original purpose of symbols was to provide globally unique values that were kept private and for internal use only. However, in the final implementation of this primitive type, symbols ended up not being private, but they did keep their value uniqueness.
We’ll address the privacy issue a bit later. As for the uniqueness of symbols, if you create two different symbols using the factory function Symbol()
, their values will not be equal.
const symbol1 = Symbol('1'); const symbol2 = Symbol('2'); console.log(symbol1 === symbol2); // Outputs False
The data type for symbol1
and symbol2
is symbol
. You can check it by logging it into your console.
console.log(typeof(symbol1)); // Outputs symbol console.log(typeof(symbol2)); // Outputs symbol
The Symbol()
function can take a string parameter, but this parameter has no effect on the value of the symbol; it’s there just for descriptive purposes. So this string is useful for debugging since it provides you with a reference when you print the symbol, but it’s nothing but a label.
console.log(symbol1); // Outputs Symbol(symbol1) console.log(symbol2); // Outputs Symbol(symbol1)
You may be wondering why the Symbol()
function doesn’t use the new keyword to create a new symbol. You wouldn’t write const symbol = new Symbol()
because Symbol()
is a function, not a constructor.
const symbol3 = new Symbol('symbol3'); // Outputs: Uncaught TypeError: Symbol is not a constructor
Since symbols are primitives and thus immutable, the value of a symbol cannot be changed, just like the value of a number-type primitive can’t be changed.
Here’s a practical example, first with a number primitive:
let prim1 = 10; console.log(prim1); // Outputs 10 prim1 = 20; console.log(prim1); // Outputs 20 10 = 20 // Outputs: Uncaught ReferenceError: Invalid left-hand side in assignment 10 == 20 // Outputs: False
We’re assigning the prim1
variable the value 10
, which is a number primitive. We can reassign the variable prim1
with a different value, so we can say that we want our prim1
variable to have the value of 20
instead of 10
.
However, we cannot assign the value 20
to the number primitive 10
. Both 10
and 20
are number-type primitives, so they can’t be mutated.
The same applies to symbols. We can reassign a variable that has a symbol value to another symbol value, but we cannot mutate the value of the actual symbol primitive.
let symb4 = Symbol('4'); let symb5 = Symbol('5'); symb4 = symb5; console.log(symb4); // Outputs Symbol(5) Symbol(4) = Symbol(5); // Outputs: ReferenceError: Invalid left-hand side in assignment
With most primitives, the value is always exactly equal to other primitives with an equivalent value.
const a = 10; const b = 10; a == b; // Outputs True a === b; // Outputs True const str1 = 'abc'; const str2 = 'abc'; str1 == str2; // Outputs True str1 === str2; // Outputs True
However, object data types are never equal to other object types; they each have their own identity.
let obj1 = { 'id': 1 }; let obj2 = { 'id': 1 }; obj1 == obj2; // Outputs False obj1 === obj2; // Outputs False
You would expect symbols to behave like number- or string-type primitives, but they behave like objects from this point of view because each symbol has a unique identity.
let symbol1 = Symbol('1'); let symbol2 = Symbol('2'); symbol1 == symbol2; // Outputs False symbol1 === symbol2; // Outputs False
So what makes symbols unique? They are primitives, but they behave like objects when it comes to their value. This is extremely important to keep in mind when discussing the practical uses of symbols.
As mentioned earlier, symbols were intended to be unique, private values. However, they ended up not being private. You can see them if you print the object or use the Object.getOwnPropertySymbols()
method.
This method returns an array of all the symbol properties found in the object.
let obj = {}; let sym = Symbol(); obj['name'] = 'name'; obj[sym] = 'symbol'; console.log(obj);
However, notice that the symbol is not visible to the for
loop, so it’s skipped when the iteration takes place.
for (let item in obj) { console.log(item) }; // Outputs name Object.getOwnPropertySymbols(obj);
In the same way, symbols are not part of the Object.keys()
or Object.getOwnPropertyNames()
results.
Also, if you try to convert the object to a JSON string, the symbol will be skipped.
let obj = {}; let sym = Symbol(); obj['name'] = 'name'; obj[sym] = 'symbol'; console.log(obj); console.log(JSON.stringify(obj));
So symbols aren’t quite private, but they can only be accessed in certain ways. Are they still useful? When and how are they used in real life?
Most commonly, symbols are used in two cases:
Let’s see what each scenario looks like in practice.
For this use case, we’ll do a simple exercise in which we pretend to be a national travel advisory that issues travel safety recommendations. You can see the code here.
Let’s say we have a color-coded system to represent the various danger levels for a particular region.
We don’t want these codes and their values to be mistakenly overwritten, so we’ll define the following variables.
const id = Symbol('id'); const RED = Symbol('Red'); const ORANGE = Symbol('Orange'); const YELLOW = Symbol('Yellow'); const GREEN = Symbol('Green'); const redMsg = Symbol('Do not travel'); const orangeMsg = Symbol('Only travel if necessary'); const yellowMsg = Symbol('Travel, but be careful'); const greenMsg = Symbol('Travel, and enjoy your trip'); let colorCodes = [{ [id]: RED, name: RED.description, message: redMsg.description, }, { [id]: ORANGE, name: ORANGE.description, message: orangeMsg.description, }, { [id]: YELLOW, name: YELLOW.description, message: yellowMsg.description, }, { [id]: GREEN, name: GREEN.description, message: greenMsg.description, } ] let alerts = colorCodes.map(element => { return (`It is Code ${element.name}. Our recommendation for this region: ${element.message}.`); }); let ul = document.getElementById("msgList"); for (let elem in alerts) { let msg = alerts[elem]; let li = document.createElement('li'); li.appendChild(document.createTextNode(msg)); ul.appendChild(li); }
The corresponding HTML and SCSS fragments for this exercise are as follows.
<div> <h1>Alert messages</h1> <ul id="msgList"></ul> </div> ul { list-style: none; display: flex; flex: row wrap; justify-content: center; align-items: stretch; align-content: center; } li { flex-basis: 25%; margin: 10px; padding: 10px; &:nth-child(1) { background-color: red; } &:nth-child(2) { background-color: orange; } &:nth-child(3) { background-color: yellow; } &:nth-child(4) { background-color: green; } }
If you log colorCodes
, you’ll see that the ID and its value are both symbols, so they’re not displayed when retrieving the data as JSON.
It’s therefore extremely hard to mistakenly overwrite the ID of this color code or the value itself unless you know that they are there or you retrieve them, as described earlier.
Before symbols were introduced, object keys were always strings, so they were easy to overwrite. Also, it was common to have name conflicts when using multiple libraries.
Imagine you have an application with two different libraries trying to add properties to an object. Or, maybe you’re using JSON data from a third party and you want to attach a unique userID
property to each object.
If your object already has a key called userID
, you’ll end up overwriting it and thus losing the original value. In the example below, the userID
had an initial value that was overwritten.
let user = {}; user.userName = 'User name'; user.userID = 123123123; let hiddenID = Symbol(); user[hiddenID] = 9998763; console.log(user);
If you look at the user object above, you’ll see that it also has a **Symbol(): 9998763
property. This is the [hiddenID]
key, which is actually a symbol. Since this doesn’t show up in the JSON, it’s hard to overwrite it. Also, you can’t overwrite this value when there’s no description attached to the symbol as string.
user[] = 'overwritten?'; // Outputs SyntaxError: Unexpected token ] user[Symbol()] = 'overwritten?'; console.log(user);
Both symbols were added to this object, so our attempt to overwrite the original symbol with the value 99987
failed.
There’s one more caveat that makes symbols less useful than they were meant to be originally. If you declare a new Symbol()
, the value is unique indeed, but if you use the Symbol.for()
method, you’ll create a new value in the global symbol registry.
This value can be retrieved by simply calling the method Symbol.for(key)
, if it already exists. If you check the uniqueness of the variables assigned such values, you’ll see that they’re not actually unique.
let unique1 = Symbol.for('unique1'); let unique2 = Symbol.for('unique1'); unique1 == unique2; // Outputs True unique1 == unique2; // Outputs True Symbol.for('unique1') == Symbol.for('unique1'); // Outputs True Symbol.for('unique1') === Symbol.for('unique1'); // Outputs True
Moreover, if you have two different variables that have equal values and you assign Symbol.for()
methods to both of them, you’ll still get equality.
let fstKey = 1; let secKey = 1; Symbol.for(fstKey) == Symbol.for(secKey); // Outputs True Symbol.for(fstKey) === Symbol.for(secKey); // Outputs True
This can be beneficial when you want to use the same values for variables such as IDs and share them between applications, or if you want to define some protocols that apply only to variables sharing the same key.
You should now have a basic understanding of when and where you can use symbols. Be aware that even if they’re not directly visible or retrievable in JSON format, they can still be read since symbols don’t provide real property privacy or security.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]