OdeToCode IC Logo

Symbols in ES6

Monday, January 26, 2015

In a previous post we looked at the for of loop in ES6, but left an open question. What magic allows for of to find an iterator? Alternatively, what makes an object iterable? To answer either question requires some knowledge of symbols.

A symbol is a new, primitive data type in ES6. There are two key points to understand about symbols.

  1. A symbol is unique and immutable.
  2. You can use symbols as identifiers when adding properties to an object.

To understand the implications of these two statements, and how they relate to iterable objects, let’s start by creating a new symbol. Creating a new symbol is as easy as invoking Symbol as a function.

let s = Symbol();
expect(typeof s).toBe("symbol");

Note that you do not use the new operator when creating a symbol, you only invoke the function. Otherwise, the runtime will raise an exception.

let makeSymbol = () => new Symbol(); 
expect(makeSymbol).toThrow();

What’s important to remember is that each symbol you create is unique and unalterable.

let s1 = Symbol(); 
let s2 = Symbol(); 

expect(s1).toEqual(s1); 
expect(s1).not.toEqual(s2); 

When you create a symbol, you can add a description by passing a string to the Symbol function, but symbols created with the same description will be unique (even if the stringified versions look the same).

let s1 = Symbol("some description"); 
let s2 = Symbol("some description"); 

expect(s1).toEqual(s1); 
expect(s1).not.toEqual(s2); 
expect(s1.toString()).toBe("Symbol(some description)"); 
expect(s2.toString()).toBe("Symbol(some description)");

The Important Part

You can use symbols as keys into an object. In the following code we create one property using a string ("lastName"), and one with a symbol (the firstName symbol).

let firstName = Symbol(); 

let person = { 
    lastName: "Allen", 
    [firstName]: "Scott", 
}; 

expect(person.lastName).toBe("Allen"); 
expect(person[firstName]).toBe("Scott");

Using symbols as keys is where symbols become interesting. Because symbols are unique, a symbol can create a unique property inside an object. You never have to worry about conflicting with existing methods, or being accidently overwritten. ES5 libraries would use timestamps or random numbers to achieve this same effect, but symbols are provided for this exact purpose.

Members of an object created using a symbol will not appear as part of the object when using a for in loop.

let firstName = Symbol(); 

let person = { 
    lastName: "Allen", 
    [firstName]: "Scott", 
}; 

let names = []; 
for(var p in person) { 
    names.push(p); 
} 

expect(names.length).toBe(1); 
expect(names[0]).toBe("lastName"); 

Symbols also won’t appear when using Object.getOwnPropertyNames.

expect(Object.getOwnPropertyNames(person)).toEqual(["lastName"]);

Nor will they appear when serializing an object to JSON.

expect(JSON.stringify(person)).toBe('{"lastName":"Allen"}');

For these reasons, you might think symbols are useful for hiding information in an object. Although using symbols to hide information is useful, there is a new object method named getOwnPropertySymbols that will allow programmers to reflect into symbol properties that are otherwise hidden.

expect(Object.getOwnPropertySymbols(person)).toEqual([firstName]); 

let symbol0 = Object.getOwnPropertySymbols(person)[0]; 
expect(person[symbol0]).toBe("Scott"); 

Back to the question at hand. How do symbols relate to iterators and iterable objects?

Iterables and @@iterator

The JavaScript specification defines special methods to use in specific circumstances. The @@iterator method is one such example. If an object has an @@iterator method (which will return an iterator object), the object is iterable and will work as the source object in a for of loop.

How do we know if an object has an @@iterator method?

By checking to see if an object has a member at Symbol.iterator.

The iterator property attached to the Symbol function is what’s called a well-known symbol. The following code will check to see if strings, arrays, and numbers are iterable by looking for this well-known iterator method.

let site = "OdeToCode.com"; 
let values = [1,2,3,4]; 
let number = 45; 

expect(site[Symbol.iterator]).toBeDefined(); 
expect(values[Symbol.iterator]).toBeDefined(); 
expect(number[Symbol.iterator]).toBeUndefined(); 

The tests will show that strings and arrays are iterable, but numbers are not.

Now that we understand symbols, and the well-known iterator method, we can rewrite the following code.

let sum = 0; 
let numbers = [1,2,3,4]; 

for(let n of numbers) { 
    sum += n; 
} 

expect(sum).toBe(10); 

We will rewrite the code to represent what is really happening behind the scenes.

let sum = 0; 
let numbers = [1,2,3,4]; 

let iterator = numbers[Symbol.iterator](); 
let next = iterator.next(); 

while(!next.done) { 
    sum += next.value; 
    next = iterator.next(); 
} 

expect(sum).toBe(10); 

Using your knowledge of symbols to write low-level iterator code probably won’t be the most exciting application of ES6 symbols, but using symbols to make new iterable objects is exciting, and the topic for a future post.