allBlogsList

Exploring ES6 Symbols

What exactly are "symbols" and how are they useful?

A Symbol is a new JavaScript type that was introduced in the ECMAScript 2015 specification. It joins the ranks of string, number, boolean, undefined, and null as the sixth primitive type. In this post we'll go over what they are, as well as how to use them.

What is a Symbol?

A symbol is a globally unique value that enables "metaprogramming" in JavaScript. They were originally proposed as a means of creating private object properties, but the goal of privacy was dropped.

Creating Symbols

A symbol can be created by calling the global Symbol() function:

const foo = Symbol();

Using Symbols

A symbol can be used as a computed property name:

const type = Symbol();
const vehicle = {};

vehicle[type] = 'car';
console.log(vehicle[type]); // "car"

... one benefit of symbols being globally unique - is that you don't have to worry about naming collisions.

Well-Known Symbols

"Well-known" symbols represent common behaviors in JavaScript that were previously inaccessible to us as developers. There are over a dozen of them, but I'll only be covering two of them here - Symbol.hasInstance and Symbol.iterator.

Symbol.hasInstance

Every JavaScript function has a Symbol.hasInstance method, which is inherited from the Function prototype. For example, the following code snippet:

obj instanceof MyFunc

... is equivalent to:

MyFunc[Symbol.hasInstance](obj)

What this means is you can essentially rewrite the instanceof operator behavior for any object. Let's say you want to define a function that only claims objects as instances if they have a foo property:

function MyFunc(props) {
    // ...    
}

Object.defineProperty(MyFunc, Symbol.hasInstance, {
    value: function(v) {
        return !!v.foo;
    }
});

let obj = new MyFunc();
let obj2 = new MyFunc();

obj2.foo = 'bar';

console.log(obj instanceof MyFunc); // false
console.log(obj2 instanceof MyFunc); // true

... once again, rewrite the behavior of an instanceof operator. This was previously not possible as it was considered an "internal only" operation. Not convinced yet? Let's take a look at another well-known symbol.

Symbol.iterator

The Symbol.iterator symbol lets you override the behavior of an existing iterable, and also allows you to create an interable out of a non-iterable object.

I'm going to quickly pause here and say that if you are unfamiliar with iterators or generator functions - I'd suggest reading about them first before continuing to the examples below.

By default, you cannot iterate over the values of an object using a for ... of loop:

const obj = {
    foo: 'foo',
    bar: 'bar',
};

for (let value of obj) { 
    console.log(val); // Uncaught TypeError: obj is not iterable
}

However thanks to ES6 symbols, you can make this object iterable by adding a Symbol.iterator property containing a generator function:

const obj = {
    foo: 'foo',
    bar: 'bar',

    *[Symbol.iterator]() {
        for (let prop in this) {
            yield this[prop];
        }
    }
};

for (let val of obj) { 
    console.log(val); // works!
}

Summary

ES6 symbols allow us to create globally unique object properties without worrying about naming collisions. Additionally, "well-known" ES6 symbols allow us to tap into native JavaScript functionality that was previously hidden from us as developers. With these newfound powers, it will be interesting to see what new JavaScript programming patterns emerge as the usage of these symbols grow over time.