Skip to main content

Symbol, Iterator and Generator in JavaScript

6 min read

Older Article

This article was published 7 years ago. Some information may be outdated or no longer applicable.

We’re going to look at Symbols, Iterators and Generators in JavaScript (ES2015) and build a practical example that ties all three together.

Symbol

Symbol is a primitive type in JavaScript that guarantees a unique value. We don’t know (or care) what the value is. We just know it’s 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 is Object.

Symbols are used with object properties. JavaScript gives us two flavours: built-in Symbols (for iteration, for instance) and custom Symbols where we specify our own iteration logic via Symbol.iterator.

Creating a Symbol

Creating one takes a single line:

const s = Symbol();
console.log(s); // Symbol()

Remember, Symbols are unique:

const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false

Symbols shine when used with objects. Sometimes you want a property that’s “secret,” one that shouldn’t be changed or discovered easily. The old convention was to prefix it with __ (double underscore). Just a convention, nothing enforced:

const obj = {
  name: 'Joe',
  age: 22,
  __: 'secret property value',
};

With Symbols, we can hide properties more cleanly:

const s = Symbol();
const obj = {
  name: 'Joe',
  age: 22,
};
obj[s] = 'secret property value';

Now calling Object.getOwnPropertyNames(obj) returns ['name', 'age'] only. The Symbol-keyed property stays hidden.

Another method Object.getOwnPropertySymbols(obj) does return the symbol itself: [Symbol()].

Try Symbol

Iterator

An iterator lets us step through a collection. That collection could be an array or anything else. JavaScript ships with a built-in Iterator protocol, but it also lets us create custom iterators.

For a custom iterator to be valid, it must implement the iterable protocol (via Symbol.iterator).

A basic example

The simplest iterator prints out array values:

const numbers = [1, 2, 3];
for (const number of numbers) {
  console.log(number);
}

The for...of construct uses the Iterator protocol under the hood.

Let’s push it a step further:

const numbers = [1, 2, 3];
const it = numbers[Symbol.iterator]();
console.log(it.next());

This returns { value: 1 done: false }. Two properties: value holds the current element, and done tells us whether the iteration has finished.

Calling it.next() again returns the same structure with value set to 2. Keep calling until you get an object where done is true and value is undefined. That means the collection is exhausted.

The for...of construct does exactly this behind the scenes, calling .next() automatically.

A custom iterator

Let’s build one. Start with an object:

const obj = {
  numbers,
};

Right now obj holds the numbers array from earlier. Let’s extend it:

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 method syntax and computed properties from ES2015, we add a Symbol.iterator method. It returns an iterator object with a next method (required by the protocol). We track our position in the array using the idx property.

The logic is simple: grab the current number, increment idx, and return { value: ..., done: false }. Once we’ve run out of numbers, return { done: true }.

Test it with for...of:

for (const number of obj) {
  console.log(number);
}

Since we control the iterator, we can do whatever we want inside it. How about filtering to only prime numbers?

Add this function to the object:

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 extend the next() implementation:

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 };
  }
};

The line return this[Symbol.iterator]().next(); skips non-prime numbers. Primes get returned normally with their value.

Run this and only primes come out:

// update the numbers array to have more values:
const numbers = [...Array(50 + 1).keys()];

for (const prime of obj) {
  console.log(prime);
}

We can also use the spread syntax, because it relies on the Iterator protocol too:

const primes = [...obj];
console.log(primes);

Try the iterator

Generator

A generator is a special function marked with *. It returns a generator object and can pause and resume execution. The yield keyword handles the pausing.

How does this connect? A generator function’s body only runs when stepped through by an iterator. The iterator’s next() call triggers each yield.

Here’s a generator that keeps producing IDs:

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.

Think back to our custom iterator. If we could build it manually, why not swap in a generator to make the code shorter? We absolutely can:

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 [Symbol.iterator]() is now a generator function. Because it’s a generator, we can yield only the prime numbers and skip everything else.

Try generator

Conclusion

Three ES2015 features: Symbols, Iterators and Generators. On their own, each one is useful but fairly narrow. Combine them and they become genuinely powerful, opening up patterns that make JavaScript a more expressive language.