Iterator and for…of loops

Article directory

  • 18. Iterator and for…of loops
    • 1. The concept of Iterator
    • 2. Default Iterator interface
      • ☆ Array, String, Map, Set, TypedArray, arguments, NodeList
    • 3. When calling the Iterator interface
      • (1) Destructuring assignment
      • (2) Extension operator
      • (3)yield*
      • (4) Other occasions
    • 4. String Iterator interface
    • 5. Iterator interface and Generator function. . . yield?
    • 6. Return(), throw() of the traverser object —- I don’t understand
    • 7. for…of loop: a summary!
      • 7.1 Array
      • 7.2 Set and Map structures
      • ☆ for…of traversal order: in the order in which each member is added to the data structure
      • 7.3 Data structure generated by calculation
      • 7.4 Array-like objects: strings, DOM NodeList, arguments
      • 7.5 Objects
      • 7.6 Comparison with other traversal syntax: traditional for and forEach cannot return midway
    • END

18. Iterator and for…of loop

1. The concept of Iterator

JavaScript’s original data structures representing “collections” are mainly arrays (Array) and objects (Object). ES6 adds Map and Set. In this way, there are four data collections, and users can use them in combination to define their own data structures. For example, the members of an array are Maps, and the members of Maps are objects. This requires a unified interface mechanism to handle all different data structures.

Iterator is such a mechanism. It is an interface that provides a unified access mechanism for various different data structures. As long as any data structure deploys the Iterator interface, it can complete the traversal operation (that is, process all members of the data structure in sequence).

Iterator has three functions:
One is to provide a unified and simple access interface for various data structures;
The second is to enable the members of the data structure to be arranged in a certain order;
Third, ES6 created a new traversal command for...of loop, and the Iterator interface is mainly used for for...of consumption.

The traversal process of Iterator is like this.
(1) Create a pointer object, pointing to the starting position of the current data structure. In other words, the traverser object is essentially a pointer object.

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

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

(4) Keep calling the next method of the pointer object until it points to the end of the data structure.

Each time the next method is called, information about the current members of the data structure will be returned. Specifically, it returns an object containing two properties: value and done. Among them, the value attribute is the value of the current member, and the done attribute is a Boolean value indicating whether the traversal has ended.

Below is an example of simulating the return value of the next method.

var it = makeIterator(['a', 'b']); // Return the return body to it

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 {<!-- --> // The return body of the function. Why can nextIndex be used in the returned object after the return body is returned?
    next: function() {<!-- -->
      return nextIndex < array.length?
        {<!-- -->value: array[nextIndex + + ], done: false} :
        {<!-- -->value: undefined, done: true};
    }
  };
}

The above code defines a makeIterator function, which is a traverser generation function and its function is to return a traverser object. Executing this function on the array ['a', 'b'] will return the array’s iterator object (i.e. pointer object) it.

The next method of the pointer object is used to move the pointer. Initially, the pointer points to the beginning of the array. Then, each time the next method is called, the pointer will point to the next member of the array. The first call points to a; the second call points 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 at 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 iterator object, the done: false and value: undefined attributes can be omitted, so the above makeIterator function can be abbreviated as form below.

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

Since Iterator only adds interface specifications to the data structure, the iterator and the data structure it traverses are actually separate. It is completely possible to write a traverser object without a corresponding data structure, or use a traverser Objects simulate data structures. Below is an example of a traverser object that runs infinitely.

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 traverser generates the function idMaker and returns a traverser object (i.e. pointer object). But there is no corresponding data structure, or in other words, the traverser object itself describes a data structure.
—-
If you use TypeScript writing, the specifications of the iterator interface (Iterable), pointer object (Iterator) and next method return value can be described as follows.
xxx

2. Default Iterator interface

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

As long as a data structure deploys the Iterator interface, we call this data structure "traversable" (iterable).

ES6 stipulates that the default Iterator interface is deployed in the Symbol.iterator attribute of the data structure, or in other words, a data structure only needs to have the Symbol.iteratorattribute , it can be considered “iterable”. The Symbol.iterator attribute itself is a function, which is the default iterator generation function of the current data structure. Executing this function will return a traverser.

As for the attribute name Symbol.iterator, it is a expression that returns the iterator attribute of the Symbol object. This is a predefined special value of type Symbol, so should be placed within square brackets (see chapter “Symbol”). —-? ? (I feel like I still have to look at the code to understand it in detail)

const obj = {<!-- -->
  [Symbol.iterator] : function () {<!-- --> // Expression as attribute name obj['a' + 'bc'] = 123;
    return {<!-- -->
      next: function () {<!-- -->
        return {<!-- -->
          value: 1,
          done: true
        };
      }
    };
  }
};

In the above code, the object obj is iterable (iterable) because it has the Symbol.iterator attribute. Executing this property will return a traverser object. The fundamental characteristic of this object is that it has a 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 native Iterator interfaces (such as arrays), that is, they can be traversed by for…of loops without any processing. The reason is that these data structures are natively deployed with the Symbol.iterator attribute (see details below), other data structures do not (such as objects).

Any data structure that deploys the Symbol.iterator attribute is said to have deployed a traverser interface. Calling this interface will return a traverser object.
—-

☆ Array, String, Map, Set, TypedArray, arguments, NodeList

The native data structure with Iterator interface is as follows.

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • the arguments object of the 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 iterator interface and is deployed in the Symbol.iterator attribute< of arr /mark>above. So, by calling this property, you get the traverser object.

For data structures that deploy the Iterator interface natively, there is no need to write the traverser generation function yourself, the for...of loop will automatically traverse them. In addition, the Iterator interface of other data structures (mainly objects) needs to be deployed on the Symbol.iterator attribute, so that it will be for...ofLoop through.

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

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

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 first deploys the Symbol.iterator method on the prototype chain of the constructor. Calling this method will return the iterator object iterator. Call the next method of the object. While returning a value, the internal The pointer moves to the next instance.
—-
Here is another example of adding an Iterator interface to an object.

3. When calling the Iterator interface

There are some occasions where the Iterator interface (i.e. Symbol.iterator method) is called by default. In addition to the for...of loop introduced below, there are several other occasions.

(1) Destructuring assignment

When destructuring 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 spread 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 spread operator in the above code calls the Iterator interface internally.

In fact, this provides a convenient mechanism to convert any data structure that implements the Iterator interface into an array. That is to say, as long as a data structure deploys the Iterator interface, you can use the spread operator on it to convert it into an array.

let arr = [...iterable];

(3)yield*

yield* is followed by a traversable structure, which calls the traverser interface of the structure.

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 array traversal will call the traverser interface, any occasion that accepts an array as a parameter actually calls the traverser interface. Here are some examples.

  • for…of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet() (such as new Map([[a’,1],[b’,2]]))
  • Promise.all()
  • Promise.race()

4. String Iterator interface

A string is an array-like object that also has an Iterator interface natively.

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, calling the Symbol.iterator method returns a traverser object, on which the next method can be called to implement string traversal.

The native Symbol.iterator method can be overridden 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 the string str has been modified, so the value returned by the spread operator (…) becomes bye, while the string itself is still hi.

5. Iterator interface and Generator function. . . yield?

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

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

// Or use the following concise writing method

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 requires almost no code deployment, as long as the yield command is used to give the return value of each step.

6. Return(), throw() of the traverser object —- I don’t understand

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

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

function readLinesSync(file) {<!-- -->
  return {<!-- -->
    [Symbol.iterator]() {<!-- -->
      return {<!-- -->
        next() {<!-- -->
          return {<!-- --> done: false };
        },
        return() {<!-- --> // What does this return mean in the object? How to trigger
          file.close();
          return {<!-- --> done: true };
        }
      };
    },
  };
}

In the above code, the function readLinesSync accepts a file object as a parameter and returns a traverser object. In addition to the next() method, return()< is also deployed. /code>method. The following two situations will trigger the execution of the return() method.

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

// case two
for (let line of readLinesSync(fileName)) {<!-- -->
  console.log(line);
  throw new Error();
}

In the above code, after the first line of the file is output in case one, the return() method will be executed to close the file; in case two, the return() method will be executed to close the file. file before throwing an error.

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. This method is not used by general traverser objects. See the chapter "Generator Functions".

7. for…of loop: a summary!

Borrowing from the C++, Java, C#, and Python languages, ES6 introduces the for...of loop as a unified way to traverse all data structures.

As long as a data structure is deployed with the Symbol.iterator attribute, it is considered to have the iterator interface, and its members can be traversed using a for...of loop. In other words, what is called inside the for...of loop is the Symbol.iterator method of the data structure.

for...of loops can be used in arrays, Set and Map structures, certain array-like objects (such as arguments objects, DOM NodeList objects), the following Generator objects, and strings .
----

7.1 Array

Arrays natively have an iterator interface (that is, the Symbol.iterator property is deployed by default). The for...of loop is essentially the traverser 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 property of the array arr, and the result is obj's for...of loop produces exactly the same result as arr.

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

forEach method: https://www.runoob.com/jsref/jsref-foreach.html

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

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

JavaScript's original for...in loop can only obtain the key name of the object, but cannot directly obtain the key value. 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 for...in loops to read the key name, and for...of loops to read 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 (see "Array "Extensions" chapter).

for...of loop calls the traverser interface. The traverser interface of the array only returns attributes with numeric indexes. This is also 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 will not return the foo attribute of the array arr.
----

7.2 Set and Map structures

Set and Map structures also have native Iterator interfaces and can directly use for...of loops.

☆ for…of traversal order: in the order in which each member is added to the data structure

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 Map structure. There are two things worth noting. First, the order of traversal is in the order in which each member is added to the data structure. Secondly, when the Set structure is traversed, a value is returned, while when the Map structure is traversed, 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

----

7.3 Data structure generated by calculation

Some data structures are calculated and generated based on existing data structures. For example, ES6's array, Set, and Map all deploy the following three methods, which all return traverser objects after being called.

  • entries() returns a traverser object used to traverse the array composed of [key name, key value]. For an array, the key name is the index value; for a 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 a traverser object used to traverse all key names.
  • values() returns a traverser object used to traverse all key values.

The traverser objects generated after calling these three methods traverse the data structures generated by calculations.
----

7.4 Array-like objects: strings, DOM NodeList, arguments

There are several categories of array-like objects. The following is an example of for...of loop used for strings, DOM NodeList objects, and arguments objects.

// 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, another feature of the for...of loop is that it will correctly identify 32-bit UTF-16 characters.

for (let x of 'a\\?\\?') {<!-- -->
  console.log(x);
}
// 'a'
// '\\?\\?'

Not all array-like objects have the Iterator interface. A simple solution is to use the Array.from method to convert it to an array.

let arrayLike = {<!-- --> length: 2, 0: 'a', 1: 'b' };

// Report an error
for (let x of arrayLike) {<!-- -->
  console.log(x);
}

// correct
for (let x of Array.from(arrayLike)) {<!-- -->
  console.log(x);

----

7.5 Objects

For ordinary objects, the for...of structure cannot be used directly and an error will be reported. The Iterator interface must be deployed before it can be used. However, in this case, the for...in loop can still be used to traverse the key names.

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 key names, 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 iterate through the array.

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

Another approach is to repackage the object using a Generator function.

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

----

7.6 Comparison with other traversal syntax: traditional for and forEach cannot return midway

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

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

This way of writing is cumbersome, so arrays provide built-in forEach method.

myArray.forEach(function (value) {<!-- -->
  console.log(value);
});

The problem with this way of writing is that you cannot break out of the forEach loop midway, and neither the break command nor the return command will work.

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

The for...in loop has several disadvantages.

  • The key names of the array are numbers, but the for...in loop uses strings as the key names "0", "1", "2", etc.
  • The for...in loop not only traverses the numeric key names, but also traverses other manually added keys, even keys on the prototype chain.
  • In some cases, a for...in loop will iterate over the keys in any order.

In short, the for...in loop is mainly designed for traversing objects and is not suitable for traversing arrays.
----
The for...of loop has some significant advantages compared to the above methods.

  • It has the same concise syntax as for...in, but does not have the shortcomings of for...in.
  • Unlike the forEach method, it can be used with break, continue and return.
  • Provides a unified operation interface for traversing all data structures.

END

syntaxbug.com © 2021 All Rights Reserved.