DEV Community

Paceaux
Paceaux

Posted on

The Symbology of JavaScript Symbols

Alt Text

There have been two things that have appeared in newer versions of JavaScript that I haven't understood: Symbols and Iterators.

When it comes to code I have to learn by doing, no amount of reading Mozilla Developer Network was going to help me understand Symbols.

So I finally decided to stop reading articles, crack open a JavaScript console, snap in to a Slim Jim, and figure out what Symbols are all about.

And now that I think I understand some basics, I want to share them with you.

JavaScript Symbols are a new primitive

If you read the entry on the Mozilla Developer Network it'll tell you right there at the beginning:

Symbol is a primitive data type

That's a big deal. The Symbol is right up there with String, Boolean, Number, undefined, and nobody's favorite, null.

Primitives are a bit like the protons and electrons; you mix and match them to make atoms (objects). Put enough of them together and you can make a dying Death Star (it's called NPM).

So the fact that I've been ignoring Symbol is bad; I've been ignoring a fundamental piece of how we build in the JavaScript universe.

Symbols are unequivocally unique

This is something that's easy to read on (digital) paper, but maybe hard to accept in practice: When you create a Symbol it is unique. Forever and for-always.

let ianMalcom = Symbol('Ian Malcom');
let ianMalcomClone = Symbol('Ian Malcom');

const ianMalcomWasWrong = (ianMalcom == ianMalcomClone); // false
const michaelCrichtonWasWrong = (ianMalcom === ianMalcomClone); // false
Enter fullscreen mode Exit fullscreen mode

Not only is every symbol ever created unique, you can't even compare two symbols with the same "value".

Symbols don't coerce

Remember everyone's favorite gag, that old 1 + '1' == '11'? Or [1] + 1 == 11? I know you remember the one about the boolean throwing the baseball at the window: true * ([1] + [1]) == 11

Those are all fun type coercion games that we hope never come up in job interviews.

Well guess what?

Symbol don't play like that.

const zildjian = Symbol('1');
zildjian + 1; // TypeError: can't convert symbol to number
zildjian + '1'; // TypeError: can't convert symbol to string
!!zildjian; // true. Huh.
Enter fullscreen mode Exit fullscreen mode

So Symbols don't give into our concatenation shenanigans.

So what are Symbols actually good for?

What good is an absolutely unique primitive that doesn't give into JavaScript's Weird Coercion Tricks®?

Symbols are Secret(ish)

Let's suppose some sort of library that we're bringing into our codebase. We're making some dinosaurs, but maybe we don't have that bit about getting dinos to reproduce.

The old school way might be something like this:

import dinosaurs from 'dinosaurs';

const tRex = Object.assign(
  {
    reproduce() {
     return null;
    }
  },
  dinosaurs); 
Enter fullscreen mode Exit fullscreen mode

This seems fine, except ... it's easy to create a scenario where everyone dies:

// Step 1. Let's create a bigger t-rex
const megaTRex = Object.assign({}, tRex);

// Step 2. t-rexes get lonely because they have no one to hug
megaTRex.reproduce = function () {
  return this;
};

// Step 3. You know what no one asked for? Velociraptor + t-rex
const velociTrex = Object.assign(velociraptor, megaTrex);


// Step 4. Hey, turns out we've got this .reproduce function, WCGW?
velociTrex.reproduce(); // returns zero rescues from Chris Pratt

Enter fullscreen mode Exit fullscreen mode

Sometimes, when we add a feature on to an object, we want it just for that object. We don't want others to see what we've been doing and to use it themselves.

Symbol is a secret password

What if we created our own private way to help that megaTRex reproduce? One that no one else could know about?

Let's go back to our megaTRex and try again. But this time, we'll use a Symbol as the key for our object (this is called a symbol-keyed property):


const megaReproducer= Symbol('reproduce');

const megaTRex= Object.assign({}, tRex);

megaTRex[megaReproducer] = function () { 
  return this;
}

Enter fullscreen mode Exit fullscreen mode

Guess what? You're not going to find that on the object! If you trying to find this megaReproducer with for-in, you're not going to find it.

In the Firefox console, you'll see something like this if you inspect the object:

Alt Text

You can see that it's there, but you're not going to find it in any of your typical approaches you might think of for finding properties on an object or its prototype.

None of these will reveal a property-keyed Symbol:

for (property in megaTRex) {
 console.log(megaTrex[property])
}

Object.keys(megaTRex).forEach(property => console.log(propertyName));

for (let [propertyName, value] of Object.entries(megaTRex)) {
  console.log(propertyName, value);
}
Enter fullscreen mode Exit fullscreen mode

You'll have to use Object.getOwnPropertySymbols if you want to find the symbols living on an object. So it's not like Symbol-keyed property is invisible; it's just chilling on an island you weren't thinking of looking on.

But, looking is one thing. How do you access it?

It takes a symbol to know a symbol

You might be able to inspect an object and see that a symbol is a key on it. But you don't have any way to access it — unless you have the symbol you used to create it.

What this means is that we get a good fair amount of privacy and protection for our objects. The Symbol is a way to extend an object, maybe even an object you don't really "own" -- and do it in a safe way where you don't open up risks of abuse elsewhere in your runtime.

The only way this megaTRex is going to make a baby T-rex is if you have that exact Symbol:

const newMegaTRex = megaTrex[megaReproducer]()
Enter fullscreen mode Exit fullscreen mode

This is especially practical in cases where we are importing from a library and exporting code to be used elsewhere:

import {tRex} from 'dinosaurs.js';
const keyToReproduction = Symbol('frogs');

tRex[keyToReproduction] = function () {

    return this;

}

export const megaTRex = tRex[keyToReproduction]();
Enter fullscreen mode Exit fullscreen mode

We've safely extended our library and exported a product after that extension — without exporting the ability to access that function.

Symbols don't collide

Going back to the fact that a Symbol is absolutely unique. This turns out to be handy for another reason: it prevents accidental overwrites.

Let's step away from uncited Michael Crichton fan-fiction for a minute and talk through a slightly more practical example: extending the console.

Let's suppose we're dealing with a particularly fickle library, and we need to do a lot of logging.

Maybe we want to have a nice formatted console.log, because we're developers and of course we'd want this.

let pretty= Symbol('logPretty');

console.log[pretty] = function (message, styles= 'font-size: 1.5em; color: #bad') {
  console.log(`%c ${message}`, styles);
}
Enter fullscreen mode Exit fullscreen mode

Cool. Because we've used a Symbol for extending the console, we're safe from any browser ever adding console.log.pretty in the near or distant future.

This is a great way to extend globals in the browser!

As long as we've got access to that pretty variable, we can write console.log[pretty]('Hey there') and see all the delightful console messages we want.

Just, uh, remember that you need that exact symbol. Which means...

Avoid Collisions with const

You may have noticed that I used let in my example above.
This is bad. Don't do that.

// uh oh. I reassigned my variable
pretty = Symbol('newPretty');
console.log[pretty] = function (message, styles = 'font-size: 3em; color: red') {
  console.log(`%c ${message}`, styles)
}
Enter fullscreen mode Exit fullscreen mode

Now I have no easy way to get back my old "pretty" symbol.

I should've used const so my variable couldn't be reassigned. That was dumb.

Retrieving lost Symbols

How can I ever outshine other developers on my team without this precious symbol? Will I ever be able to get my long lost symbol back, so that I can make my logs pretty again?

Of course. I need to use getOwnPropertySymbols and quit being melodramatic:

const [oldPretty, newPretty] = Object.getOwnPropertySymbols(console.log);

Enter fullscreen mode Exit fullscreen mode

Which reminds me...

Describe your Symbols

Wen you create a Symbol, it doesn't need a 'descriptor'. You can make plain ol' ordinary undescriptive Symbols:

const prettyBad = Symbol();

Enter fullscreen mode Exit fullscreen mode

Much like cloning dinosaurs, this is probably a bad idea.

If you're using Symbol-keyed properties, and you need to use getOwnPropertySymbols, that descriptor is going to be the key to figuring out which is the prettiest log of them all:

Alt Text

I should add, by the way, that while you could use the same descriptor for every symbol-keyed property, that doesn't mean you should:

Alt Text

Symbols don't stringify()

JSON.stringify ignores Symbols completely.

import { dna } from 'dinosaurs';

const reproduction = Symbol('frogs');
const howToCloneDinosaurs = {
  richDudes: 1,
  newman: 0,
  cynicalMathematicians: 1,
  paleontologists: 2,
  island: 'isla nublar',
  lawyers: Infinity
};

howToCloneDinosaurs[reproduction] = dna;

Enter fullscreen mode Exit fullscreen mode

console output of json.stringify where lawyers are deservedly null and a symbol-keyed property isn't there

I think this is a Good Thing™.

It prevents cloning

The most common way to deep-clone objects in JavaScript is with JSON.parse(JSON.stringify()).

A Symbol-keyed property is a simple and terse way to put a property on an object that you don't want to be cloned.

Of course, you can also use the always-clunky, super verbose, always-have-to-look-it-up Object.defineProperty() to make a property unJSON.stringifiable:

Object.defineProperty(howToCloneDinosaurs,'reproduction', {
 value: dna,
 enumerable: false
});
Enter fullscreen mode Exit fullscreen mode

Object.defineProperty might make sense when we need to define a lot of things about a property. But if we want an easy way to make sure the property and its value aren't cloned, Symbol seems the way to go.

There's some built-in Symbols

Turns out, there's a slew of "built-in" symbols that exist. I won't list them all here, but there's a few that catch my eye as particularly interesting:

  • Symbol.iterator
  • Symbol.asyncIterator
  • Symbol.split
  • Symbol.toStringTag

The reason these are of interest to me (and should be of interest to you) is because these are "Symbol Keys" that allow us to define our own behaviors on objects. These behaviors didn't used to be available to us, but now they are!

Create a String that iterates by word

for of is kinda awesome, but it only works on things that are iterable (more on what that means in another post.

Let's use Symbol.iterator and make a string iterable:

function WordString(text) {
    const string = new String(text); // make explicit object
    const words = string.split(' '); // split by spaces
    let wordIndex = 0;

    string[Symbol.iterator] = function* stringIterator() {
      while (wordIndex < words.length) {
       yield words[wordIndex++]
        .replace(new RegExp('[!.?]', 'g'),''); // remove any punctuation
      }
    }

    return string;
}

Enter fullscreen mode Exit fullscreen mode

Ignore the * and the yield for right now. Those are things for iterators. Just dial in on the fact that we used a global Symbol key (Symbol.iterator) and we used it to make something that wasn't iterable ... iterable.

Look at what we can do with this fancy WordString now:

Output in a console using for-of where each word in the string

Create an Honest Array

If you read my previous post on arrays you might recall that there's an implicit and explicit undefined. Maybe you're disappointed that arrays are liars sometimes.

Let's use Symbol.species to tell us that this is still an array. And then we'll throw a generator function on that array and define what for of will actually return:


class TruthyArray extends Array {
    constructor(value) {
        super(...value);  
        this.value = [...value];
    }
    get [Symbol.species]() {
      return Array;
    }
    *[Symbol.iterator]() {
      let itemIndex = -1;
          while (itemIndex < this.value.length ) {
              if (this.value[++itemIndex]) {
                  yield this.value[itemIndex];
              }
          }
      }
  }

Enter fullscreen mode Exit fullscreen mode

Again, ignore the * and the yield. That's for another time.

The greater point is that Symbol has some built-in "keys" that we can add to an object to extend functionality.

The Recap

Dev.to says this is a 9-minute read. That's like 2 cigarette breaks or one visit to the bathroom after a tryst with a holiday cheese plate.

I don't want to keep you much longer or someone will be looking for you... and, "I was reading an article about Frank's Jurassic Park-inspired JavaScript Symbol fan fiction," is not how you want to explain yourself. You're a professional.

  1. Symbol is a primitive. It's worth knowing because it's in the guts of how everything works in JS now.
  2. Symbols are unique and it's best not to think about how they're unique. But they are.
  3. Symbols don't coerce into other things. They laugh in the face of your + jokes.
  4. You can and probably should be making property-keyed symbols.
  5. Symbol-keyed properties guarantee you'll never have collision; that makes it the perfect way to extend browser built-ins or libraries.
  6. Symbol-keyed properties are hidden-ish. JSON.stringify ignores them, for-in, Object.keys ignores them, too. You have to know you're looking for a property-keyed Symbol.
  7. You need your Symbol to access a symbol-keyed property, so use const for defining it, or otherwise make sure you throw descriptors on that sucker lest it become lost forever and foralways.
  8. It's not just about Symbol. The Symbol is how we access utilities previously unavailable to us on objects.

Shoutouts

Thanks to Isabela Moreira and Alex Klock for providing technical review of this.

Top comments (1)

Collapse
 
andyghiuta profile image
Andy G

This is the best way I have seen Symbol explained. Good article. Thanks!