Symbol
is a built-in object whose constructor returns asymbol
primitive β also called a Symbol value or just a Symbol β thatβs guaranteed to be unique. Symbols are often used to add unique property keys to an object that wonβt collide with keys any other code might add to the object, and which are hidden from any mechanisms other code will typically use to access the object.
In Javascript, Symbols are incredible resources for all sorts of use cases. However, I think many of the possibilities show their true colors when combined with classes. There are very many static Symbol
properties that can be used with classes, although I'll only be going through a few of the most important. Check the rest out at the MDN page linked!
All of the below will work with any object, not just classes. I think classes simply demonstrate their usefulness the best
How to use static Symbol
properties
As described in the top quote, symbols are unique. That means, if you create a symbol and attach it to an object as a property key (using bracket notation property accessors), the assigned value will only be accessible when using the same exact instance of that symbol.
const mySymbol = Symbol('foo');
const obj = {
[mySymbol]: 'bar',
};
// undefined - 'foo' is only a descriptor
// and doesn't actually do anything
obj.foo;
// undefined - all symbols are unique
obj[Symbol('foo')];
// 'bar' - π
obj[mySymbol];
With this mechanic, static Symbol
properties were created (for mostly internal use) so that classes and objects can be more configurable without taking up any property names that you could use otherwise.
1. Symbol.iterator
and Symbol.asyncIterator
This one's a biggie. Symbol.iterator
and Symbol.asyncIterator
will most notably specify the behavior of a class in for...of
and for await...of
loops respectively. Here's an example of it in action:
// a class representing a bookshelf
class Bookshelf {
// this functions is an iterator,
// so we prefix it with a `*`
// and use the `yield` keyword
*[Symbol.iterator]() {
yield 'Harry Potter';
yield 'The Tempest';
yield 'The Lion King';
}
}
In this example, we are using Symbol.iterator
to create an iterator that will be used to iterate through every book on the "bookshelf". I hard coded the values, but it a more realistic example, you'd probably want to dynamically yield
every value in a predefined array (i.e. this.books
).
class Bookshelf {
// ...
}
const bookshelf = new Bookshelf();
for (const book of bookshelf) {
console.log(book);
}
The above will log the following:
'Harry Potter'
'The Tempest'
'The Lion King'
It's like magic! The same can be used for Symbol.asyncIterator
with for await...of
2. Symbol.toStringTag
This symbol is much less confusing than the above, but still very cool. Ever wondered why Object#toString()
returns '[object Object]'
, Map#toString()
returns '[object Map]'
, etc?
Your first guess might be that it uses constructor.name
. However, we can debunk that because the following doesn't work:
class Book {}
// '[object Object]' - not '[object Book]'
new Book().toString();
Instead, they use Symbol.toStringTag
to specify what tag they want to be attached.
class Book {
get [Symbol.toStringTag]() {
return 'Book';
}
}
// '[object Book]'
new Book().toString();
Note that if you want your class to return something special when converted to a string that doesn't fit that format, you can simply overwrite the toString()
method itself.
I'm sure there are many use cases for this, but I think it's best used for debugging (especially if you're creating a library and want to make it easy for the end user to troubleshoot). If you try to print some text and you find [object Object]
, it might be hard to find out what's causing it
However, if you get [object Boolean]
, [object Null]
, or a custom [object SomeClassName]
, I bet you it will be a lot easier.
3. Symbol.hasInstance
This symbol defines the behavior of instanceof
when used with your class.
'hello world' instanceof string; // true
100 instanceof string; // false
String[Symbol.hasInstance]('hello world'); // true
String[Symbol.hasInstance](100); // false
Here's an example of implementing it yourself:
class Book {
constructor(name, author) {
this.name = name;
this.author = author;
}
// `instance` is what's being compared
static [Symbol.hasInstance](instance) {
// `instance` is a `Book` if
// it has a name and author
return book.name && book.author;
}
}
// these are the fields we need
const name = 'Harry Potter';
const author = 'J.K. Rowling';
new Book(name, author) instanceof Book; // true
{ name, author } instance of Book; // true
4. Symbol.species
This one's hard to wrap your head around. Symbol.species
is most notably used internally for arrays and maps (although you can use it in your custom classes as well) to find what subclass should be created from methods that create new classes out of themselves... or something.
Here's an example:
class CustomArray extends Array {}
const arr = new CustomArray(1, 2, 3);
// true - even though `Array#map` creates a *new* array,
// it will dynamically access the constructor through `this.constructor`,
// meaning it can automatically create derived classes when needed
console.log(arr.map((num) => num * 2) instanceof CustomArray);
But, maybe you want to override that:
class CustomArray extnds Array {
static get [Symbol.species]() {
return Array;
}
}
const arr = new CustomArray(1, 2, 3);
// false - this will now always create `Array`s
console.log(arr.map((num) => num * 2) instanceof CustomArray);
Internally, arrays are deciding what class to construct like so:
new (this.constructor[Symbol.species] || this.constructor)(/* ... */);
First it accesses Symbol.species
to see if you have an override set up, then it falls back to the current constructor.
I hope you learned one or more new way to use the Symbol
! If you have any questions, corrections, or addons, I would love to hear them. Peace β
Top comments (4)
Oh, it means that
symbol
is a kind of ID for the object in the JavaScript, but what is async and await, I don't knowcuz I am dumb.
BTW nice post π
Here's a tutorial! javascript.info/async-await
You can find countless more by simply searching "Javascript async/await" if you don't understand the link above.
Oh, thank you.
βΊοΈπ
[object Boolean]
could be found if you were to do something likeObject.prototype.toString.call(true)
Obviously this is improbable to be used nowadays, but if you're dealing with a really old library or something you might come across it.