DEV Community

Cover image for Who needs Javascript symbols?
Konstantin Meiklyar
Konstantin Meiklyar

Posted on • Edited on

Who needs Javascript symbols?

Cover image by Alexander Fradellafra from Pixabay

Symbols are a less known primitive data type among string, number, bigint, boolean and undefined of Javascript. They were added as part of ES6 specification which was a big facelifting of Javascript language and included a lot of new features.

Why do we need Symbols?

Symbols have 2 main use cases:

  1. Create hidden properties on objects that no other code (that has no reference to the symbol used) can access or overwrite. The convention of most built-in functions and libraries is to avoid referencing symbols declared on an object if there is no direct need to change them.

  2. System symbols that are used to change default behaviors of object - for example, Symbol.toPrimitive that is used to define object behavior during the conversion of an object to primitive or Symbol.iterator that is used to set object behavior during the iteration.

Symbols basics

Symbols' syntax is very symbol simple. We can create a new symbol by writing:

// mySymbol is a new created symbol
let mySymbol = Symbol();
console.log(mySymbol) // Symbol()
Enter fullscreen mode Exit fullscreen mode

Symbol() function has an optional description field and can be used in this way:

// mySymbol is a new created symbol that now has a description
let mySymbol = Symbol('decription of my symbol');
console.log(mySymbol) // Symbol(decription of my symbol)
Enter fullscreen mode Exit fullscreen mode

The description field is just a text that will be attached to the symbol - it's mostly used for debugging purposes.

Every symbol returned from Symbol() function is unique, meaning that 2 symbols created using the function will never be equal (even they have same description passed to the function):

let firstSymbol = Symbol("sameDescription");
let secondSymbol = Symbol("sameDescription");
console.log(firstSymbol == secondSymbol); //false
Enter fullscreen mode Exit fullscreen mode

Creating hidden properties in object

Now when we know how to create a new Symbol let's see how can use it to create a hidden property of an object.

Alt Text

First of all - why would we do that?

As a common use case, I can mention an example when our code is used by some third party. For example - we are writing an open-source library or a library that is going to be used by other teams of developers in our organization. We may want to add some "under-the-hood" properties to objects to be able to access them in our code - but at the same time, we want to guarantee that no other code will be able to access these properties.

If we were using regular object properties declared by a string - the developers using our library can do that accidentally by iterating over object keys or creating a property with the same name and overwriting it.

Symbols are here to help us.

For example - let's say we have an object representing a rock star:

let rockStar = {
  name: "James Hetfield",
  band: "Metallica",
  role: "Voice & Rythm guitar"
}
Enter fullscreen mode Exit fullscreen mode

Now we want to add a hidden property that will represent an internal id that we want to be exposed only in our code and avoid using it outside our internal code:

let idSymbol = Symbol('id symbol used in rockStar object');

let rockStar = {
  name: "James Hetfield",
  band: "Metallica",
  role: "Voice & Rythm guitar"
  [idSymbol]: "this-id-property-is-set-by-symbol"
}
Enter fullscreen mode Exit fullscreen mode

If we now want to access / change / delete the property set using the Symbol - we need to have the reference to the Symbol that was used to declare it. Without having it - we can't do that.

Also - when iterating over the keys of an object - we will not get a reference to a property set using the Symbol:

console.log(Object.keys(rockStar)); // (3) ["name", "band", "role"]
Enter fullscreen mode Exit fullscreen mode

for ... in ... loop will also ignore our symbol:

for (key in rockStar) {
    console.log(key);
}

// output:
// name
// band
// role

Enter fullscreen mode Exit fullscreen mode

Global symbol registry

What if in some cases we do want to add an ability to give access to properties that were defined using symbols? What if we need to share access to these properties between different modules of our application?

This is where Global symbol registry comes to help us. Think of it as a dictionary placed on a global level - accessible everywhere in our code where we can set or get Symbols by a specific key.

Symbol.for is a syntax used to get Symbols from the global registry.

Let's take the same example and re-write it using the global registry:

let idSymbol = Symbol.for('rockStarIdSymbol');

let rockStar = {
  name: "James Hetfield",
  band: "Metallica",
  role: "Voice & Rythm guitar"
  [idSymbol]: "this-id-property-is-set-by-symbol"
}
Enter fullscreen mode Exit fullscreen mode

let idSymbol = Symbol.for('rockStarIdSymbol'); will do the following:

  1. Check if the global registry has a symbol related to the key that equals rockStarIdSymbol and if there is one - return it
  2. If not - create a new symbol, store it in the registry and return it.

This means, that if we will need to access our property in any other place in the code we can do the following:

let newSymbol = Symbol.for('rockStarIdSymbol');
console.log(rockStar[newSymbol]); // "this-id-property-is-set-by-symbol"
Enter fullscreen mode Exit fullscreen mode

As a result - worth mentioning that 2 different Symbols returned by the same key in the global registry will be equal:

let symbol1 = Symbol.for('rockStarIdSymbol');
let symbol2 = Symbol.for('rockStarIdSymbol');
console.log(symbol1 === symbol2); // true
Enter fullscreen mode Exit fullscreen mode

There is also a way to check which key Symbol is related to in the global registry using Symbol.keyFor function.

const symbolForRockstar = Symbol.for('rockStarIdSymbol')
console.log(Symbol.keyFor(symbolForRockstar)); //rockStarIdSymbol
Enter fullscreen mode Exit fullscreen mode

Symbol.keyFor is checking the global registry and finds the key for the symbol. If the symbol is not registered in the registry - undefined will be returned.

System symbols

System symbols are symbols that can be used to customize the behavior of objects. The full list of system symbols can be found in latest language specification. Each system symbol gives access to some specification which behavior we can overwrite and customize.

Alt Text

As an example - let's see a usage one of the commonly used symbols - Symbol.iterator that gives us access to the iterator specification.

Let's assume we want to write a Javascript class representing a music band.
It will probably have a band's name, style, and a list of band members.

class Band {
   constructor(name, style, members) {
     this.name = name;
     this.style = style;
     this.members = members;
   }
}
Enter fullscreen mode Exit fullscreen mode

And we will be able to create a new instance of the class by writing something like this:

const metallicaBand = new Band('Metallica', 'Heavy metal', 
['James', 'Lars', 'Kirk', 'Robert'];
Enter fullscreen mode Exit fullscreen mode

What if we'll want our users to be able to iterate of the instance of the class like it was an array and get the names of band members? This behavior is reused in a few libraries having arrays wrapped inside objects.

Right now - if we will try to iterate over our object using a for ... of loop - we will get an error saying Uncaught TypeError: "metallicaBand" is not iterable. That's because our class definition has no instruction on how this iteration should be done. If we do want to enable iteration over it - we need to set the behavior and Symbol.iterator is a system symbol that we should use.

Let's add it to our class definition:

class Band {
   constructor(name, style, members) {
     this.name = name;
     this.style = style;
     this.members = members;
   }

  [Symbol.iterator]() { 
    return new BandIterator(this);
  }
}

class BandIterator{
  // iterator implementation
}
Enter fullscreen mode Exit fullscreen mode

I will not dive into the actual implementation of the iterator - this can be a good topic for a separate post. But talking of Symbols - that's the use case we should know. Almost every native behavior can be changed and system symbols are the way to do it in javascript classes.

What else?

1) Well, technically properties on objects that are set using symbols are not 100% hidden. There are methods Object.getOwnPropertySymbols(obj), that returns all symbols set on an object and Reflect.ownKeys(obj) that lists all properties of on object, including symbols. But the common convention is not to use these methods for listing, iteration, and any other generic actions performed on objects.

2) Few times I saw code that had symbols used to declare enum values, like:

const ColorEnum = Object.freeze({
  RED: Symbol("RED"), 
  BLUE: Symbol("BLUE")
});
Enter fullscreen mode Exit fullscreen mode

Not sure how good this practice is. Assuming that Symbols are not serializable and every attempt to stringify these values will just remove them from the object.

When using symbols - use serialization carefully. And overall - avoid making deep copies using JSON.parse(JSON.stringify(...)). This approach sometimes can cause hard to catch bugs that are causing sleepless nights!

3) Function used for shallow object cloning - Object.assign copies both symbols and regular string properties. This sounds like a proper design behavior.

I think that's all you need to know about symbols to have the full picture. Did I forget anything?

Alt Text

Happy you made it until this point!

Thanks for reading, as usual, I will appreciate any feedback.

If you love Javascript as I do - visit https://watcherapp.online/ - my side project having all javascript blog posts in one place, there is a ton of interesting stuff!

Top comments (0)