Iterator and for...of loop

Posted by Joopy on Tue, 07 Dec 2021 13:27:07 +0100

Iterator and for... of loop

Concept of Iterator

JavaScript's original data structures representing "sets" are mainly arrays and objects. ES6 adds Map and Set. In this way, there are four data sets. Users can also combine them to define their own data structure. For example, the members of the Array are Map and the members of the Map are objects. This requires a unified interface mechanism to deal with all different data structures.

Iterator is such a mechanism. It is an interface that provides a unified access mechanism for various data structures. As long as the iterator interface is deployed for any data structure, the traversal operation can be completed (that is, all members of the data structure can be processed in turn).

Iterator has three functions: one is to provide a unified and simple access interface for various data structures; Second, the members of the data structure can be arranged in a certain order; Third, ES6 creates a new traversal command for...of loop, and the iterator interface is mainly used for for...of consumption.

The Iterator traversal process is like this.

(1) Create a pointer object to the starting position of the current data structure. That is, the traverser object is essentially a pointer object.

(2) By calling the next method of the pointer object for the first time, you can point the pointer to the first member of the data structure.

(3) The second time the next method of the pointer object is called, the pointer points to the second member of the data structure.

(4) The next method of the pointer object is called until it points to the end of the data structure.

Every time the next method is called, the information of the current member of the data structure will be returned. Specifically, it returns an object containing two attributes: value and done. The value attribute is the value of the current member, and the done attribute is a Boolean value indicating whether the traversal is over.

The following is an example that simulates the return value of the next method.

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}

The above code defines a makeIterator function, which is an ergodic generator function to return an ergodic object. Executing this function on the array ['a ','b'] will return the ergodic object (i.e. pointer object) it of the array.

The next method of the pointer object, which is used to move the pointer. At the beginning, the pointer points to the beginning of the array. Then, each time the next method is called, the pointer points to the next member of the array. The first call, pointing to a; The second call, pointing to b.

The next method returns an object representing the information of the current data member. This object has two attributes: value and done. The value attribute returns the member of the current position. The done attribute is a Boolean value indicating whether the traversal is over, that is, whether it is necessary to call the next method again.

In short, by calling the next method of the pointer object, you can traverse the data structure given in advance.

For the ergodic object, the done: false and value: undefined attributes can be omitted, so the makeIterator function above can be abbreviated to the following form.

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++]} :
        {done: true};
    }
  };
}

Because the Iterator only adds the interface specification to the data structure, the Iterator is actually separated from the data structure it traverses. It can write the Iterator object without corresponding data structure, or simulate the data structure with the Iterator object. The following is an example of an infinite running ergodic object.

var it = idMaker();

it.next().value // 0
it.next().value // 1
it.next().value // 2
// ...

function idMaker() {
  var index = 0;

  return {
    next: function() {
      return {value: index++, done: false};
    }
  };
}

In the above example, the iterator generates the function idMaker and returns an iterator object (that is, a pointer object). But there is no corresponding data structure, or the traverser object describes a data structure itself.

If TypeScript is used, the specifications of the return values of the Iterator interface (iteratable), the pointer object (Iterator) and the next method can be described as follows.

interface Iterable {
  [Symbol.iterator]() : Iterator,
}

interface Iterator {
  next(value?: any) : IterationResult,
}

interface IterationResult {
  value: any,
  done: boolean,
}

Default Iterator interface

The purpose of Iterator interface is to provide a unified access mechanism for all data structures, that is, for...of loop (see below). When a for...of loop is used to traverse a data structure, the loop will automatically look for the Iterator interface.

A data structure is called "iteratable" as long as the Iterator interface is deployed.

ES6 stipulates that the default iterator interface is deployed in the Symbol.iterator attribute of the data structure, or a data structure can be considered "iteratable" as long as it has the Symbol.iterator attribute. The Symbol.iterator property itself is a function, which is the default iterator generation function of the current data structure. Executing this function returns an iterator. As for the attribute name Symbol.iterator, it is an expression that returns the iterator attribute of the Symbol object. This is a predefined special value of type Symbol, so it should be placed in square brackets.

const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
};

In the above code, the object obj is iteratable because it has the Symbol.iterator attribute. Executing this property returns an iterator object. The fundamental feature of this object is that it has the next method. Each time the next method is called, an information object representing the current member will be returned, with two attributes: value and done.

Some data structures in ES6 have a native iterator interface (such as an array), that is, they can be traversed by a for...of loop without any processing. The reason is that these data structures natively deploy the Symbol.iterator attribute (see below), while some other data structures do not (such as objects). All data structures that deploy the symbol. Iterator attribute are called deployed ergodic interfaces. Calling this interface will return an iterator object.

The data structure with native Iterator interface is as follows.

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • arguments object for function
  • NodeList object

The following example is the Symbol.iterator property of an array.

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

In the above code, the variable arr is an array, which has a native ergodic interface and is deployed on the Symbol.iterator attribute of arr. Therefore, by calling this property, you get the iterator object.

For the data structures of the native deployed Iterator interface, you do not need to write the ergodic generation function yourself, and the for...of loop will automatically traverse them. In addition, the Iterator interfaces of other data structures (mainly objects) need to be deployed on the Symbol.iterator attribute, so that they can be traversed by the for...of loop.

The reason why an Object does not deploy the Iterator interface by default is that it is uncertain which attribute of the Object is traversed first and which attribute is traversed later, which needs to be manually specified by the developer. In essence, the ergodic is a linear process. For any nonlinear data structure, deploying the ergodic interface is equivalent to deploying a linear transformation. However, strictly speaking, the Object deployment ergodic interface is not necessary, because at this time, the Object is actually used as a Map structure. ES5 does not have a Map structure, but ES6 natively provides it.

If an object wants to have an Iterator interface that can be called by the for...of loop, it must deploy an Iterator generation method on the attribute of Symbol.iterator (objects on the prototype chain can also have this method).

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    }
    return {done: true, value: undefined};
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
  console.log(value); // 0, 1, 2
}

The above code is the writing method of a class deployment Iterator interface. The Symbol.iterator property corresponds to a function that returns the Iterator object of the current object after execution.

The following is an example of implementing the "linked list" structure through the traverser.

function Obj(value) {
  this.value = value;
  this.next = null;
}

Obj.prototype[Symbol.iterator] = function() {
  var iterator = { next: next };

  var current = this;

  function next() {
    if (current) {
      var value = current.value;
      current = current.next;
      return { done: false, value: value };
    }
    return { done: true };
  }
  return iterator;
}

var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);

one.next = two;
two.next = three;

for (var i of one){
  console.log(i); // 1, 2, 3
}

The above code first deploys the Symbol.iterator method on the prototype chain of the constructor. Calling this method will return the iterator object, and call the next method of the object to automatically move the internal pointer to the next instance while returning a value.

Here is another example of adding an Iterator interface to an object.

let obj = {
  data: [ 'hello', 'world' ],
  [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

For array like objects (with numeric key name and length attribute), there is a simple way to deploy the Iterator interface, that is, the Symbol.iterator method directly refers to the Iterator interface of the array.

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

[...document.querySelectorAll('div')] // It's ready to go

NodeList object is an array like object. It has a traversal interface and can be traversed directly. In the above code, we changed its traversal interface to the Symbol.iterator attribute of the array. We can see that it has no impact.

The following is an example of another array like object calling the Symbol.iterator method of the array.

let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}

Note that the Symbol.iterator method for deploying an array of ordinary objects has no effect.

let iterable = {
  a: 'a',
  b: 'b',
  c: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // undefined, undefined, undefined
}

If the Symbol.iterator method does not correspond to an ergodic generation function (that is, an ergodic object will be returned), the interpretation engine will report an error.

var obj = {};

obj[Symbol.iterator] = () => 1;

[...obj] // TypeError: [] is not a function

In the above code, the Symbol.iterator method of variable obj does not correspond to the iterator generation function, so an error is reported.

With the ergodic interface, the data structure can be traversed with a for...of loop (see below) or a while loop.

var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
  var x = $result.value;
  // ...
  $result = $iterator.next();
}

In the above code, ITERABLE represents some kind of traversable data structure, and $iterator is its iterator object. Each time the traverser object moves the pointer (next method), check the done attribute of the return value. If the traversal is not finished, move the pointer of the traverser object to the next step (next method) and keep cycling.

When calling the Iterator interface

On some occasions, the iterator interface (i.e. Symbol.iterator method) will be called by default. In addition to the for...of loop described below, there are several other occasions.

(1) Deconstruction assignment

When deconstructing and assigning values to arrays and Set structures, the Symbol.iterator method will be called by default.

let set = new Set().add('a').add('b').add('c');

let [x,y] = set;
// x='a'; y='b'

let [first, ...rest] = set;
// first='a'; rest=['b','c'];

(2) Extension operator

The extension operator (...) also calls the default Iterator interface.

// Example 1
var str = 'hello';
[...str] //  ['h','e','l','l','o']

// Example 2
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']

The Iterator interface is called inside the extension operator of the above code.

In fact, this provides a simple mechanism to convert any data structure with the Iterator interface deployed into an array. That is, as long as a data structure has the Iterator interface deployed, you can use the extension operator to convert it into an array.

let arr = [...iterable];

(3)yield*

yield * is followed by a traversable structure that calls the structure's traverser interface.

let generator = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};

var iterator = generator();

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }

(4) Other occasions

Since the traversal of the array will call the ergodic interface, any occasion that accepts the array as a parameter actually calls the ergodic interface. Here are some examples.

  • for...of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet() (for example, new Map([['a',1],['b',2]])
  • Promise.all()
  • Promise.race()

Iterator interface for string

A string is an array like object that also has a native Iterator interface.

var someString = "hi";
typeof someString[Symbol.iterator]
// "function"

var iterator = someString[Symbol.iterator]();

iterator.next()  // { value: "h", done: false }
iterator.next()  // { value: "i", done: false }
iterator.next()  // { value: undefined, done: true }

In the above code, the Symbol.iterator method is called to return an ergodicator object. On this traverser, we can call the next method to achieve the traversal of the string.

You can override the native Symbol.iterator method to modify the behavior of the iterator.

var str = new String("hi");

[...str] // ["h", "i"]

str[Symbol.iterator] = function() {
  return {
    next: function() {
      if (this._first) {
        this._first = false;
        return { value: "bye", done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
};

[...str] // ["bye"]
str // "hi"

In the above code, the Symbol.iterator method of string str is modified, so the value returned by the extension operator (...) becomes bye, and the string itself is hi.

Iterator interface and Generator function

The simplest implementation of the Symbol.iterator() method is to use the Generator function.

let myIterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
    yield 3;
  }
};
[...myIterable] // [1, 2, 3]

// Or use the following concise wording

let obj = {
  * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};

for (let x of obj) {
  console.log(x);
}
// "hello"
// "world"

In the above code, the Symbol.iterator() method hardly needs to deploy any code, just use the yield command to give the return value of each step.

Ergodic object return(), throw()

In addition to the next() method, the traverser object can also have a return() method and a throw() method. If you write the ergodic object generation function yourself, the next() method must be deployed, and whether the return() method and throw() method are deployed is optional.

The use of the return() method is that if the for...of loop exits early (usually because of an error or a break statement), the return() method will be called. If an object needs to clean up or release resources before completing traversal, you can deploy the return() method.

function readLinesSync(file) {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          return { done: false };
        },
        return() {
          file.close();
          return { done: true };
        }
      };
    },
  };
}

In the above code, the function readLinesSync takes a file object as a parameter and returns an ergodic object. In addition to the next() method, the return() method is also deployed. The following two cases will trigger the execution of the return() method.

// Situation 1
for (let line of readLinesSync(fileName)) {
  console.log(line);
  break;
}

// Situation II
for (let line of readLinesSync(fileName)) {
  console.log(line);
  throw new Error();
}

In the above code, after the first line of the output file, the return() method will be executed to close the file; Case 2 will throw an error after executing the return() method to close the file.

Note that the return() method must return an object, which is determined by the Generator syntax.

The throw() method is mainly used in conjunction with the Generator function, which is not used by general ergodic objects.

for... of loop

ES6 uses C + +, Java, c# and Python languages for reference and introduces the for...of loop as a unified method to traverse all data structures.

As long as a data structure deploys the Symbol.iterator attribute, it is regarded as having an iterator interface, and its members can be traversed by a for...of loop. That is, the Symbol.iterator method of the data structure is called inside the for...of loop.

The for...of loop can use arrays, Set and Map structures, some array like objects (such as arguments object, DOM NodeList object), Generator object, and string.

array

The array has a native iterator interface (that is, the Symbol.iterator attribute is deployed by default). The for...of loop is essentially an iterator generated by calling this interface, which can be proved by the following code.

const arr = ['red', 'green', 'blue'];

for(let v of arr) {
  console.log(v); // red green blue
}

const obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);

for(let v of obj) {
  console.log(v); // red green blue
}

In the above code, the empty object obj deploys the Symbol.iterator attribute of array arr, and the for...of loop of obj produces exactly the same result as arr.

The for...of loop can replace the forEach method of an array instance.

const arr = ['red', 'green', 'blue'];

arr.forEach(function (element, index) {
  console.log(element); // red green blue
  console.log(index);   // 0 1 2
});

The original for...in loop of JavaScript can only obtain the key name of the object, not the key value directly. ES6 provides a for...of loop that allows traversal to obtain key values.

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}

The above code shows that the for...in loop reads the key name and the for...of loop reads the key value. If you want to get the index of the array through the for...of loop, you can use the entries method and keys method of the array instance.

The for...of loop calls the ergodic interface, and the ergodic interface of the array returns only the attributes with numerical indexes. This is different from the for...in loop.

let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i in arr) {
  console.log(i); // "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}

In the above code, the for...of loop does not return the foo attribute of array arr.

Set and Map structures

Set and Map structures also have a native Iterator interface, which can directly use the for...of loop.

var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
  console.log(e);
}
// Gecko
// Trident
// Webkit

var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
  console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262

The above code demonstrates how to traverse the Set structure and the Map structure. There are two things worth noting. First, the traversal order is the order in which each member is added to the data structure. Secondly, when traversing the Set structure, a value is returned, while when traversing the Map structure, an array is returned. The two members of the array are the key name and key value of the current Map member.

let map = new Map().set('a', 1).set('b', 2);
for (let pair of map) {
  console.log(pair);
}
// ['a', 1]
// ['b', 2]

for (let [key, value] of map) {
  console.log(key + ' : ' + value);
}
// a : 1
// b : 2

Calculation generated data structure

Some data structures are generated by calculation on the basis of existing data structures. For example, the array, Set, and Map of ES6 deploy the following three methods, which return the traverser object after calling.

  • entries() returns an Iterator object that is used to iterate over an array of [key name, key value]. For arrays, the key name is the index value; For Set, the key name is the same as the key value. The Iterator interface of the Map structure calls the entries method by default.
  • keys() returns an iterator object to iterate over all key names.
  • values() returns an iterator object that is used to iterate over all key values.

The traverser objects generated after these three method calls are all the data structures generated by calculation.

let arr = ['a', 'b', 'c'];
for (let pair of arr.entries()) {
  console.log(pair);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']

Array like objects

Array like objects include several classes. The following is an example of a for...of loop for strings, DOM NodeList objects, and arguments objects.

// character string
let str = "hello";

for (let s of str) {
  console.log(s); // h e l l o
}

// DOM NodeList object
let paras = document.querySelectorAll("p");

for (let p of paras) {
  p.classList.add("test");
}

// arguments object
function printArgs() {
  for (let x of arguments) {
    console.log(x);
  }
}
printArgs('a', 'b');
// 'a'
// 'b'

For strings, the for...of loop also has a feature that it can correctly recognize 32-bit UTF-16 characters.

for (let x of 'a\uD83D\uDC0A') {
  console.log(x);
}
// 'a'
// '\uD83D\uDC0A'

Not all array like objects have the Iterator interface. A simple solution is to use the Array.from method to convert them into arrays.

let arrayLike = { length: 2, 0: 'a', 1: 'b' };

// report errors
for (let x of arrayLike) {
  console.log(x);
}

// correct
for (let x of Array.from(arrayLike)) {
  console.log(x);
}

object

For ordinary objects, the for...of structure cannot be used directly and an error will be reported. It can only be used after the Iterator interface is deployed. However, in this case, the for...in loop can still be used to traverse the key name.

let es6 = {
  edition: 6,
  committee: "TC39",
  standard: "ECMA-262"
};

for (let e in es6) {
  console.log(e);
}
// edition
// committee
// standard

for (let e of es6) {
  console.log(e);
}
// TypeError: es6[Symbol.iterator] is not a function

The above code indicates that for ordinary objects, the for...in loop can traverse the key name, and the for...of loop will report an error.

One solution is to use the Object.keys method to generate an array of object key names, and then traverse the array.

for (var key of Object.keys(someObject)) {
  console.log(key + ': ' + someObject[key]);
}

Another way is to use the Generator function to repackage the object.

const obj = { a: 1, b: 2, c: 3 }

function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}

for (let [key, value] of entries(obj)) {
  console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3

Comparison with other traversal grammars

Taking arrays as an example, JavaScript provides a variety of traversal syntax. The most primitive way of writing is the for loop.

for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

This method is troublesome, so the array provides a built-in forEach method.

myArray.forEach(function (value) {
  console.log(value);
});

The problem with this way of writing is that you can't jump out of the forEach loop halfway, and neither the break command nor the return command works.

The for...in loop can traverse the key names of the array.

for (var index in myArray) {
  console.log(myArray[index]);
}

The for...in loop has several disadvantages.

  • The key name of an array is a number, but the for...in loop takes a string as the key name "0", "1", "2", and so on.
  • The for...in loop traverses not only numeric key names, but also other keys added manually, even those on the prototype chain.
  • In some cases, the for...in loop iterates over key names in any order.

In short, the for...in loop is mainly designed to traverse objects and is not suitable for traversing arrays.

The for...of loop has some significant advantages over the above methods.

for (let value of myArray) {
  console.log(value);
}
  • It has the same concise syntax as for...in, but it does not have the disadvantages of for...in.
  • Unlike the forEach method, it can be used with break, continue, and return.
  • It provides a unified operation interface for traversing all data structures.

The following is an example of using the break statement to jump out of the for...of loop.

for (var n of fibonacci) {
  if (n > 1000)
    break;
  console.log(n);
}

The above example will output items with Fibonacci number series less than or equal to 1000. If the current item is greater than 1000, it will use the break statement to jump out of the for...of loop.

Topics: Javascript Front-end html5