ES6 loops and iterables

Author: Alan Storm

Translation: crazy tech nerd

original: https://alanstorm.com/es6s-ma...

Reproduction without permission is strictly prohibited

This article will examine ES6's for ... of loops.

old way

In the past, there were two ways to traverse javascript.

The first is the classic for i loop, which allows you to iterate over an array or any object that is indexable and has a length property.

for(i=0;i<things.length;i++) {
    var thing = things[i]
    /* ... */
}

Next is the for ... in loop, which loops over the key/value pairs of an object.

for(key in things) {
    if(!thing.hasOwnProperty(key)) { continue; }
    var thing = things[key]
    /* ... */
}

The for ... in loop is often seen as an aside because it loops Every enumerable property of an object . This includes the properties of the parent object in the prototype chain, as well as all properties that are assigned as methods. In other words, it traverses something that one might not expect. Using for ... in usually means a lot of guard clauses in the loop block to avoid unwanted properties.

Early javascript solved this problem with libraries. Many JavaScript libraries (eg: Prototype.js, jQuery, lodash, etc.) have utility methods or functions like each or foreach that allow you to iterate over objects and arrays without for i or for ... in loops.

The for ... of loop is ES6's way of trying to solve some of these problems without using third-party libraries.

for ... of

for ... of loop

for(const thing of things) {
    /* ... */
}

It will iterate over an iterable object.

An iterable object is an object that defines an @@iterator method, and the @@iterator method returns an object that implements the iterator protocol, or the method is a generator function.

In this sentence you need to understand a lot of things:

  • iterable object
  • @@iterator method (what does @@ mean?)
  • Iterator protocol (what does the protocol here mean?)
  • Wait, iterable and iterator aren't the same thing?
  • Also, what the heck is a generator function?

These questions are addressed one by one below.

Built-in Iterable

First of all, some built-in objects in javascript objects are naturally iterable, for example, the most easily thought of is the array object. Arrays can be used in for ... of loops like in the following code:

const foo = [
'apples','oranges','pears'
]

for(const thing of foo) {
  console.log(thing)
}

The output is all elements in the array.

apples
oranges
pears

There is also the entries method for arrays, which returns an iterable object. This iterable returns keys and values ​​on each loop. For example the following code:

const foo = [
'apples','oranges','pears'
]

for(const thing of foo.entries()) {
  console.log(thing)
}

will output the following

[ 0, 'apples' ]
[ 1, 'oranges' ]
[ 2, 'pears' ]

The entries method is more useful when using the following syntax

const foo = [
    'apples','oranges','pears'
]

for(const [key, value] of foo.entries()) {
  console.log(key,':',value)
}

Two variables are declared in the for loop: one for the first item of the returned array (the key or index of the value), and one for the second item (the value that the index actually corresponds to).

A normal javascript object is not iterable. If you execute the following code:

// cannot execute properly
const foo = {
  'apples':'oranges',
  'pears':'prunes'
}

for(const [key, value] of foo) {
  console.log(key,':',value)
}

will get an error

$ node test.js
/path/to/test.js:6
for(const [key, value] of foo) {
TypeError: foo is not iterable

However, the static entries method of the global Object object accepts an ordinary object as a parameter and returns an iterable object. A program like this:

const foo = {
  'apples':'oranges',
  'pears':'prunes'
}

for(const [key, value] of Object.entries(foo)) {
  console.log(key,':',value)
}

to get the output you expect:

$ node test.js
apples : oranges
pears : prunes

Create your own Iterable

If you want to create your own iterable, it will take more time. You'll remember what I said earlier:

An iterable object is an object that defines an @@iterator method, and the @@iterator method returns an object that implements the iterator protocol, or the method is a generator function.

The easiest way to understand this is to create an iterable object step by step. First, we need an object that implements the @@iterator method. The @@ notation is a bit misleading, what we really want to do is define a method with the predefined Symbol.iterator notation.

If you define the object with iterator methods and try to iterate:

const foo = {
  [Symbol.iterator]: function() {
  }
}

for(const [key, value] of foo) {
  console.log(key, value)
}

got a new error:

for(const [key, value] of foo) {
                          ^
TypeError: Result of the Symbol.iterator method is not an object

This is javascript telling us that it is trying to call the Symbol.iterator method, but the result of the call is not an object.

To eliminate this error, iterator methods are needed to return objects that implement the iterator protocol. This means that the iterator method needs to return an object with the next key, which is a function.

const foo = {
  [Symbol.iterator]: function() {
    return {
      next: function() {
      }
    }
  }
}

for(const [key, value] of foo) {
  console.log(key, value)
}

If I run the code above, I get a new error.

for(const [key, value] of foo) {
                     ^
TypeError: Iterator result undefined is not an object

This time javascript tells us that it tried to call the Symbol.iterator method, which is indeed an object and implements the next method, but the return value of next is not the object javascript expected.

The next function needs to return an object in a specific format - with the keys value and done.

next: function() {
    //...
    return {
        done: false,
        value: 'next value'
    }
}

The done key is optional. If the value is true (meaning the iterator has finished iterating), the iteration has ended.

The value key is required if done is false or not present. The value key is the value that should be returned by looping through this.

So put another program in the code with a simple iterator that returns the first ten even numbers.

class First20Evens {
  constructor() {
    this.currentValue = 0
  }

  [Symbol.iterator]() {
    return {
      next: (function() {
        this.currentValue+=2
        if(this.currentValue > 20) {
          return {done:true}
        }
        return {
          value:this.currentValue
        }
      }).bind(this)
    }
  }
}

const foo = new First20Evens;
for(const value of foo) {
  console.log(value)
}

Builder

Manually constructing objects that implement the iterator protocol is not the only option. Generator objects (returned by generator functions) also implement the iterator protocol. The above example, built with generators, would look like this:

class First20Evens {
  constructor() {
    this.currentValue = 0
  }

  [Symbol.iterator]() {
    return function*() {
      for(let i=1;i<=10;i++) {
        if(i % 2 === 0) {
          yield i
        }
      }
    }()
  }
}

const foo = new First20Evens;
for(const item of foo) {
  console.log(item)
}

This article won't cover generators too much, if you need to get started You can see this article . The big takeaway today is that we can make our Symbol.iterator method return a generator object that "just works" in a for ... of loop. "Working properly" means that the loop can keep calling next on the generator until the generator stops yield ing values.

$ node sample-program.js
2
4
6
8
10

This article is first published on WeChat public account: Front-end Pioneer

Welcome to scan the QR code and follow the official account, we will send you fresh front-end technical articles every day

Please continue to read other highly praised articles in this column:

Tags: Javascript Front-end ECMAScript

Posted by SZero on Tue, 17 May 2022 19:07:58 +0300