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
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.
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);
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
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;
}
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:
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);
}
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]()
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]();
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);
}
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)
}
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);
Which reminds me...
Describe your Symbols
Wen you create a Symbol, it doesn't need a 'descriptor'. You can make plain ol' ordinary undescriptive Symbol
s:
const prettyBad = Symbol();
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:
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:
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;
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
});
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;
}
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:
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];
}
}
}
}
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.
-
Symbol
is a primitive. It's worth knowing because it's in the guts of how everything works in JS now. - Symbols are unique and it's best not to think about how they're unique. But they are.
- Symbols don't coerce into other things. They laugh in the face of your
+
jokes. - You can and probably should be making property-keyed symbols.
- Symbol-keyed properties guarantee you'll never have collision; that makes it the perfect way to extend browser built-ins or libraries.
- 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. - 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. - 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)
This is the best way I have seen Symbol explained. Good article. Thanks!