In this article, we'll take a look at Symbols, Iterators and Generators in JavaScript (ES2015) and we'll also implement an interesting example of using all three technologies.
Symbol
is a new primitive type in JavaScript, and it guarantees us that, at all times, it will have a unique value. What the value is, we don't care (and we don't know as we can't retrieve it), but we can be sure that it'll be unique.
The ECMAScript standard defines 7 data types, out of which 6 are primitive types in JavaScript:
Boolean
,Null
,Undefined
,Number
,String
, and of course Symbol. The seventh data type isObject
.
Symbols are used with object properties. There are two types of Symbols that JavaScript gives us: built-in Symbols - for example for iteration and the language also allows us to create a custom symbol where we can specify custom iteration logic via Symbol.iterator
.
Creating a Symbol couldn't be easier:
const s = Symbol();
console.log(s); // Symbol()
Remember that Symbols are unique:
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false
As mentioned earlier, Symbols are best used with objects. There are certain scenarios when we want to create an object, and we would like to have a property that's "secret" or a property that we don't want people to change or update. Normally these would be added by the key __
(double underscore). This is a convention, nothing more:
const obj = {
name: 'Joe',
age: 22,
__: 'secret property value',
};
Now with Symbols, we can add such properties in a much easier way:
const s = Symbol();
const obj = {
name: 'Joe',
age: 22,
};
obj[s] = 'secret property value';
Using the code above means that when we call Object.getOwnPropertyNames(obj)
on our object, we will get an array of ['name', 'age']
only but not x
. We have successfully "hidden" the property.
Another method
Object.getOwnPropertySymbols(obj)
does return the symbol itself:[Symbol()]
.
An iterator allows us to iterate through a collection. A collection, in this case, can be an array or anything else. JavaScript by default implements the Iterator protocol, but it also caters for the creation of custom iterators.
For a custom iterator to be valid, it must implement the iterable protocol (via
Symbol.iterator
).
Let's take a look at the most basic iterator that allows us to print out the values of an array:
const numbers = [1, 2, 3];
for (const number of numbers) {
console.log(number);
}
The above for...of
construct uses the Iterator protocol to get the numbers in the array.
Let's take the previous example a little bit further.
const numbers = [1, 2, 3];
const it = numbers[Symbol.iterator]();
console.log(it.next());
The code above returns a custom object structure: { value: 1 done: false }
. It's important to understand this object. It has two properties: value
which represents the first value from the array, and a done
property which indicates that the iteration has not finished.
Calling it.next()
again will return the previously seen object structure, except this time the value
property will be set to 2
. We can keep on calling it.next()
until we receive an object where the done
property will be set to true
. In that case, the value
property will have a value undefined
. This indicates that there are no more items to iterate through in the collection.
The for...of
construct does the same under the hood - it automatically calls the .next()
method to iterate through the collection.
Let's go ahead and create a custom iterator. To do this, we need to create an object:
const obj = {
numbers,
};
At the moment the obj
object has access to the numbers
array that we saw earlier. Let's extend our object now:
const obj = {
numbers,
idx: 0,
[Symbol.iterator]() {
const it = {
next: () => {
if (this.idx < this.numbers.length) {
const number = this.numbers[idx];
this.idx++;
return { value: number, done: false };
} else {
return { done: true };
}
},
};
},
};
Using the concise property method, as well as the computed property features in JavaScript ES2015 we added the object method Symbol.iterator
which requires us also to add an iterator object, which must implement a next
method. What we are doing here is simple: We make sure that our custom iterator has the next
method available since that will be required to iterate through any collection.
Because we are creating a custom iterator, we can do whatever we want. Notice that we also have an idx
property to keep track of index values from the numbers
array.
The code above manually implements what the for...of
loop is doing. It grabs the first number from the numbers
array, and it increments the idx
property. It will keep on doing this until there are elements in the numbers array. While there are elements, it returns the { value: ..., done: false }
object. Once there are no more numbers available, it changes the done
property to true
.
We can test our iterator by using the for...of
construct:
for (const number of obj) {
console.log(number);
}
Since we now dictate the functionality of our iterator, we can do cool things. For example, wouldn't it be nice if we would list only the prime numbers?
Let's extend our object by adding the following function to it:
isPrime(number) {
for (let i = 2; i < number; i++) {
if (number % i === 0) {
return false;
}
}
return number !== 1 && number !== 0;
}
A prime number is a whole number that is greater than 1 whose only factors are 1 and itself - in other words, it cannot be "created" by multiplying other whole numbers.
Now that we have a function to handle prime numbers we can also extend our implementation of next()
:
next: () => {
if (this.idx < this.numbers.length) {
const number = this.numbers[this.idx];
if (!this.isPrime(number)) {
this.idx++;
return this[Symbol.iterator]().next();
} else {
const number = this.numbers[this.idx];
this.idx++;
return { value: number, done: false };
}
} else {
return { done: true };
}
};
With return this[Symbol.iterator]().next();
we can skip a value if it's not a prime number, otherwise we return the usual object with the value
and done
properties.
Running the following code will only return the prime numbers from our array:
// update the numbers array to have more values:
const numbers = [...Array(50 + 1).keys()];
for (const prime of obj) {
console.log(prime);
}
Alternatively, we can also use the spread syntax because that also uses the Iterator protocol:
const primes = [...obj];
console.log(primes);
A generator is a particular function in JavaScript that's denoted by the *
character. The function itself returns a generator object, and it can pause and resume. With generator functions, the JavaScript language also gets the yield
keyword which is capable of pausing the executing of the function.
How does a generator function enter our discussion? A generator function's body is only called when it is going through an iterator. Remember the following: an iterator's next()
method invokes the yield
keyword in a generator function.
We can easily create a generator function to keep on generating an ID for us:
function* generateID() {
let number = 0;
while (number < number + 1) {
yield number++;
}
}
const it = generateID();
it.next(); // { value: 0, done: false }
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
Adding a for...of construct would now create a loop that would keep on generating ID numbers forever - be cautious and don't run into this mistake.
Thinking a bit back at what we developed when discussing the Iterators, watchful readers may already know what's coming. If, before we could create our Iterator, why can't we change that code and add a generator function to make the code terser? We can do that!
const numbers = [...Array(50 + 1).keys()];
const obj = {
numbers,
isPrime(number) {
for (let i = 2; i < number; i++) {
if (number % i === 0) {
return false;
}
}
return number !== 1 && number !== 0;
},
*[Symbol.iterator]() {
for (let i = 0; i < this.numbers.length; i++) {
const number = this.numbers[i];
if (this.isPrime(number)) {
yield number;
}
}
},
};
const primes = [...obj];
console.log(primes);
Notice that the [Symbol.iterator]()
is now added as a generator function. Because it is added as a generator function, we can call yield to return only the prime numbers.
This article intended to walk you through 3 new concepts added to JavaScript as part of ES2015 - Symbols, Iterators and Generators. Individually these features may not be that useful, however combining them together reveals their real power and can make JavaScript a more interesting language.