Javascript Symbols + Classes = 💖
Tags: javascript, programming, tutorial
Published: 2021-10-11 21:38:48

> [**`Symbol`**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) is a built-in object whose constructor returns a `symbol` [primitive](https://developer.mozilla.org/en-US/docs/Glossary/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.

\- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors#bracket_notation)), the assigned value will only be accessible when using the *same exact instance* of that symbol.

```js
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`

[*Learn about iterators*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
 
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:

```js
// 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`). 

```js
class Bookshelf {
  // ...
}

const bookshelf = new Bookshelf();
for (const book of bookshelf) {
  console.log(book);
}
```

The above will log the following:
```js
'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:

```js
class Book {}

// '[object Object]' - not '[object Book]'
new Book().toString();
```

Instead, they use `Symbol.toStringTag` to specify what tag they want to be attached.

```js
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.

```js
'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:

```js
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.

![Talk is cheap — show me the code](https://i.imgur.com/JGl9k1x.png)

Here's an example:

```js
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:

```js
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:

```js
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 ✌