DEV Community

Cover image for Javascript Symbols + Classes = πŸ’–
Lioness100
Lioness100

Posted on • Updated on

Javascript Symbols + Classes = πŸ’–

Symbol is a built-in object whose constructor returns a symbol 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

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];
Enter fullscreen mode Exit fullscreen mode

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

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';
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

The above will log the following:

'Harry Potter'
'The Tempest'
'The Lion King'
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Internally, arrays are deciding what class to construct like so:

new (this.constructor[Symbol.species] || this.constructor)(/* ... */);
Enter fullscreen mode Exit fullscreen mode

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 ✌

Discussion (5)

Collapse
foxy4096 profile image
Aditya Priyadarshi • Edited on

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 know
cuz I am dumb.

BTW nice post πŸ‘

Collapse
lioness100 profile image
Lioness100 Author

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.

Collapse
foxy4096 profile image
Aditya Priyadarshi

Oh, thank you.
β˜ΊοΈπŸ˜‹

Collapse
code913 profile image
code913 • Edited on

Nice article!
But I have a doubt:

[object Boolean]

You can insert a boolean inside a string, right?

Collapse
lioness100 profile image
Lioness100 Author

[object Boolean] could be found if you were to do something like Object.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.