Understanding symbols in JavaScript
Brian Neville-O'Neill
Posted on May 22, 2020
Written by Eslam Hefnawy✏️
Before symbols were introduced in ES6 as a new type of primitive, JavaScript used seven main types of data, grouped into two categories:
- Primitives, including the string, number, bigint, boolean, null, and undefined data types
- Objects, including more complex data structures, such as arrays, functions, and regular JS objects
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.
When and how are symbols used in real life?
As mentioned earlier, symbols were intended to b e 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:
- Unique property values that you don’t want users to overwrite by mistake
- Unique keys for identifying object properties
Let’s see what each scenario looks like in practice.
1. Unique property values
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.
- Code Red is the highest level; people should not travel to this region
- Code Orange is a high level; people should only travel to this region if really necessary
- Code Yellow represents a medium level of danger; people should remain vigilant when traveling to this region
- Code Green means no danger; people can safely travel to this 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.
2. Unique keys for identifying object properties
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.
Symbols are unique — until they aren’t
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.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post Understanding symbols in JavaScript appeared first on LogRocket Blog.
Posted on May 22, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.