The Complete Guide to Symbols in JavaScript

Master JavaScript Symbols, the powerful and versatile primitive data type, with this comprehensive guide. Learn everything you need to know, from the basics to advanced techniques. With Symbols, you can write more secure, efficient, and reusable code.

Before symbols were introduced in ES6 as a new type of primitive, JavaScript used seven main types of data, grouped into two categories:

  1. Primitives, including the string, number, bigint, boolean, null, and undefined data types
  2. 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 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);
Array of Symbol Properties in an Object

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.

No Symbols in Object 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));
Converting an Object to a JSON String

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:

  1. Unique property values that you don’t want users to overwrite by mistake
  2. 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);
Username Value Overwritten

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);
JavaScript Symbol Property

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.

Source: https://blog.logrocket.com/ 

#javascript

The Complete Guide to Symbols in JavaScript
23.00 GEEK