Iterators gonna iterate

ES6 gives us a new way to iterate, and it's already supported in stable releases of Firefox, Chrome, & Opera. Here it is:

for (var num of [1, 2, 3]) {
  console.log(num);
}
// Result: 1
// Result: 2
// Result: 3

Unlike for (part in thing) which iterates through property names of an object in a generic way, for (part of thing) lets the object decide which values it gives up on each iteration.

Let's pull its guts out

Pop the array on the ol' operating table there, and prepare it for surgery. How does it work? Well…

var numbers = [1, 2, 3];
numbers[Symbol.iterator];
// Result: function ArrayValues() { [native code] }

The ES6 spec defines non-string 'symbol' property names of objects to describe particular behaviours. Symbol.iterator is one of them, it describes how iteration works.

var numbersIterator = numbers[Symbol.iterator]();
numbersIterator.next();
// Result: Object {value: 1, done: false}
numbersIterator.next();
// Result: Object {value: 2, done: false}
numbersIterator.next();
// Result: Object {value: 3, done: false}
numbersIterator.next();
// Result: Object {value: undefined, done: true}

The above is what for (var num of numbers) is doing under the hood.

When we call numbers[Symbol.iterator] we get an object back with a .next method. Calling .next gives us an object containing the value, or an indication there are no further values.

Let's make our own

Let's make an object that iterates over words in a string (in an overly-simple way). Firstly the constructor:

function Words(str) {
  this._str = str;
}

Then the iterator-factory:

Words.prototype[Symbol.iterator] = function() {
  var re = /\S+/g;
  var str = this._str;
  
  return {
    next: function() {
      var match = re.exec(str);
      if (match) {
        return {value: match[0], done: false};
      }
      return {value: undefined, done: true};
    }
  }
};

We're returning an object with a next method, which returns {value: nextWordInTheString, done: false} until there are none left.

And it works!

var helloWorld = new Words("Hello world");

for (var word of helloWorld) {
  console.log(word);
}
// Result: "Hello"
// Result: "world"

Well, actually, it doesn't work in Firefox, because Firefox doesn't support the Symbol object yet, it uses the non-standard @@iterator form. You can make it work in Firefox using:

Words.prototype[self.Symbol ? Symbol.iterator : "@@iterator"] = func;

Edit: Symbol is supported in Firefox 36, which should reach stable in .

Generators

Generators, which are defined using function*, are a more convenient way of creating iterator factories.

From a generator you yield the values you want to provide, and it takes care of the value/done object for you:

function* someNumbers() {
  yield 1;
  yield 2;
  yield 3;
}

var iter = someNumbers();
iter.next();
// Result: Object {value: 1, done: false}
iter.next();
// Result: Object {value: 2, done: false}
iter.next();
// Result: Object {value: 3, done: false}
iter.next();
// Result: Object {value: undefined, done: true}

// or just:
for (var num of someNumbers()) {
  console.log(num);
}
// Result: 1
// Result: 2
// Result: 3

Although the for-of loop above looks intuitive, it only works through a bit of trickery. For-of calls the object's Symbol.iterator method to get an iterator, but we're giving for-of someNumbers() which is already an iterator. To work around this, iterators returned by generators have a Symbol.iterator method that returns itself, meaning iter[Symbol.iterator]() === iter. It's a bit odd, but it makes the code above work as expected.

Generators are usually an easier way to describe iteration. For our Words example, it's much simpler:

Words.prototype[Symbol.iterator] = function*() {
  var re = /\S+/g;
  var str = this._str;
  var match;
  while (match = re.exec(str)) {
    yield match[0];
  }
};

What's the use?

Not only does this provide a way to allow iteration to be defined on an object-by-object basis, it's also…

Lazy

You don't need to calculate all the values ahead of time, you can provide them as needed. The Words example does this, we don't seek out the next word until .next is called.

This means…

Iterators can be infinite

No no, nono no no, nono no no, no no there's no limit:

function* powersOf2() {
  var i = 2;
  yield i;
  while (true) yield i *= i;
}

for (var i of powersOf2()) {
  console.log(i);
  if (i > 10000) break;
}

…but make sure you break at some point if you're looping over them.

NodeList iteration

We've wanted to make NodeList array-like for ages, but it's been a compatibility problem. However, we don't have this problem with iterators:

for (var node of document.querySelectorAll('a')) {
  console.log(node);
}

*ahem* except Chrome & Opera don't support the above yet. Thankfully, you can polyfill it pretty easily:

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];

String iteration

String also has an iterator. This may not sound spectacular, but as Mathias Bynens points out iterating over the symbols of a string is a real pain in ES5 due to unicode issues. String's iterator makes it easy:

for (var symbol of string) {
  console.log(symbol);
}

Tasty tasty sugar

You can convert iterable objects to arrays using [...iterable].

[...new Words("Hello world")]
// Result: Array [ "Hello", "world" ]

…unfortunately Firefox is the only browser with that feature in stable.

Browser support

Browser support for these features is pretty good. It's in stable versions of Chrome, Opera, & Firefox, and development editions of IE. That means you can use it today if you're in some kind of context that only runs in modern browsers *ahem* ServiceWorker *ahem*.

For use in other browsers, you can use a transpiler such as Traceur.

Further reading

View this page on GitHub

Comments powered by Disqus

Jake Archibald in a garden with a black cat

Hello, I'm Jake and that's me there. The one that isn't a cat. I'm a developer of sorts.

Elsewhere

Contact

Feel free to throw me an email, unless you're a recruiter, or someone trying to offer me 'sponsored content' for this site, in which case write your request on a piece of paper, and fling it out the window.