The concept of an Iterator

In ES6, Map and Set are added to JavaScript’s original data structures that represent “collections”, mainly arrays and objects. There are four data sets, and users can combine them to define their own data structures, such as arrays whose members are Maps and maps whose members are objects. This requires a unified interface mechanism to handle all the different data structures.

An Iterator is one such mechanism. It is an interface that provides a unified access mechanism for various data structures. The Iterator interface can be deployed on any data structure to complete traversal (that is, processing all members of the data structure in turn).

Iterator provides a unified and simple interface for accessing various data structures. Second, the members of the data structure can be arranged in a certain order. Third, ES6 created a new traversal command for… The of loop is an Iterator interface for… Of consumption.

The Iterator traverses like this.

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

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

(3) The next call to the pointer object 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 member of the data structure is returned. Specifically, it returns an object containing both the value and done attributes. Where, the value attribute is the value of the current member, and the done attribute is a Boolean value indicating whether the traversal is complete.

Here 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}; }}; }Copy the code

The above code defines a makeIterator function, which is an iterator generator that returns an iterator object. Executing this function on the array [‘a’, ‘b’] returns the traverser object (pointer object) it for that array.

The next method of a pointer object, 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. First call, point to a; The second call, to b.

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

In short, calling the next method on the pointer object iterates through the given data structure.

For traverser objects, the done: false and value: undefined attributes can be omitted, so the makeIterator function above can be abbreviated as follows.

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

Because Iterator only adds interface specifications to the data structure, the Iterator is essentially separate from the data structure it is iterating over, and it is possible to write an Iterator object that has no corresponding data structure, or to simulate a data structure from the Iterator object. Here is an example of an infinitely running traverser 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}; }}; }Copy the code

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

Using TypeScript, the specification of the Iterable interface, pointer object, and next method return values can be described as follows.

interface Iterable { [Symbol.iterator]() : Iterator, } interface Iterator { next(value? : any) : IterationResult, } interface IterationResult { value: any, done: boolean, }Copy the code

The default Iterator interface

The purpose of the Iterator interface is to provide a unified access mechanism for all data structures. Of loops (see below). When using the for… When the of loop iterates over some data structure, the loop automatically looks for the Iterator interface.

A data structure is said to be “iterable” whenever the Iterator interface is deployed.

ES6 states that the default Iterator interface is deployed in the symbol. Iterator property of a data structure, or that a data structure can be considered “iterable” as long as it has the symbol. Iterator property. The symbol. iterator property is itself a function that is the default traverser generator for the current data structure. Executing this function returns a traverser. Iterator is an expression that returns the iterator property of the Symbol object, a special value of type Symbol that is predefined and therefore enclosed in square brackets (see Symbol in chapter).

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

In the above code, the object obj is iterable because it has the Symbol. Iterator property. Executing this property returns 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 is returned, with both value and done attributes.

Some ES6 data structures have native Iterator interfaces (such as arrays) that can be used for… The of loop traverses. The reason is that these data structures have symbol.iterator attributes deployed natively (see below), while others (such as objects) do not. Any data structure that deploys the symbol. iterator property is said to have deployed the traverser interface. Calling this interface returns a traverser object.

The data structures with the native Iterator interface are as follows.

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • The arguments object for the function
  • The 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 }
Copy the code

In the above code, the variable arr is an array with a native traverser interface deployed on the Symbol. Iterator property of the ARR. So, by calling this property, you get the traverser object.

For natively deployed Iterator interface data structures, do not write their own traverser generator functions, for… The of loop iterates through them automatically. In addition, the Iterator interface for other data structures (mainly objects) needs to be deployed on the Symbol. Iterator property. The of loop traverses.

The Iterator interface is not deployed on an Object by default because it is uncertain which attributes of the Object are iterated first and which attributes are iterated after. The developer must manually specify the Iterator interface. An traverser is essentially a linear process, and for any nonlinear data structure, deploying the traverser interface is tantamount to deploying a linear transformation. However, strictly speaking, the object deployment traverser interface is not necessary because the object is actually used as a Map structure, which ES5 does not have and ES6 natively provides.

An object that can be for… Iterator interfaces that are called by the of loop must deploy traverser generation methods on the properties of symbol. Iterator (objects on the prototype chain may have such methods).

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}Copy the code

The above code is written as a class deployable Iterator interface. The symbol. iterator property corresponds to a function that, when executed, returns the traverser object of the current object.

The following is an example of a pointer structure implemented through a 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}Copy the code

The above code first deploys the symbol. iterator method on the constructor’s prototype chain. Calling this method returns the iterator object, and calling the next method on that object, automatically moving 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 }; }}; }};Copy the code

An easy way to deploy the Iterator interface for array-like objects (with numeric keys and length attributes) is to use the symbol. Iterator method to refer directly to the Iterator interface of the array.

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; // nodelist. prototype[Symbol. Iterator] = [][Symbol. Iterator]; [... document. QuerySelectorAll (' div ')] / / can be carried outCopy the code

The NodeList object is an array-like object with a traversal interface that can be traversed directly. In the code above, we change its traversal interface to the array’s symbol. iterator property, and you can see no effect.

Here is another example of an array-like object calling the symbol. iterator method of an 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'
}
Copy the code

Note that a normal object deploys the symbol. iterator method of an array with 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
}
Copy the code

If the symbol. iterator method corresponds to anything other than an ergoer generator (that is, an ergoer object is returned), the interpretation engine will report an error.

var obj = {};

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

[...obj] // TypeError: [] is not a function
Copy the code

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

With the traverser interface, data structures can be used with for… The of loop is iterated (more on that below), or you can use the while loop.

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

In the above code, ITERABLE represents some kind of ITERABLE data structure, and $iterator is its iterator object. Each time the traverser object moves the pointer (next method), it checks the done property of the returned value. If the traversal hasn’t finished, it moves the traverser object’s pointer to the next step (next method), repeating the loop.

The context in which the Iterator interface is called

There are a few situations where the Iterator interface (symbol. Iterator method) is called by default, except for the for… Of loops, and a couple of other occasions.

(1) Deconstructing assignment

The symbol. iterator method is called by default when destructuring arrays and Set structures.

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'];
Copy the code

(2) Extended operators

Extended operators (…) The default Iterator interface is also called.

Var STR = 'hello'; STR [...] / [' h ', 'e', 'l', 'l', 'o'] / / case 2 let arr = [' b ', 'c']; ['a', ...arr, 'd'] // ['a', 'b', 'c', 'd']Copy the code

The extension operator of the above code internally calls the Iterator interface.

In effect, this provides an easy mechanism to turn any data structure with the Iterator interface deployed into an array. That is, whenever a data structure has an Iterator interface deployed, you can use the extension operator on it to turn it into an array.

let arr = [...iterable];
Copy the code

(3) the yield *

Yield * is followed by a traversable structure, which invokes the traverser interface of that structure.

let generator = function* () { yield 1; Yield * (2 and 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 }Copy the code

(4) Other occasions

Because array traversal calls the traverser interface, any situation that takes an array as an argument actually calls the traverser interface. Here are some examples.

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

Iterator interface for strings

A string is an array-like object that also natively has an 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 }
Copy the code

In the above code, calling the symbol. iterator method returns an iterator object on which the next method can be called to iterate over 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"
Copy the code

In the above code, the symbol. iterator method of string STR has been modified, so the extension operator (…) The returned value becomes bye, and the string itself remains hi.

Iterator interface and Generator functions

The simplest implementation of the symbol.iterator () method uses the Generator function described in the next chapter.

let myIterable = { [Symbol.iterator]: function* () { yield 1; yield 2; yield 3; }}; [...myIterable] // [1, 2, 3] // Let obj = {* [symbol.iterator]() {yield 'hello'; yield 'world'; }}; for (let x of obj) { console.log(x); } // "hello" // "world"Copy the code

In the code above, the symbol.iterator () method hardly deploys any code, just yield the return value for each step.

Iterator object return(), throw()

An iterator object can have return() and throw() methods in addition to the next() method. If you write your own traverser object generation function, the next() method is mandatory, and the return() and throw() methods are optional.

The return() method is used if for… The return() method is called when the of loop exits prematurely (usually because of an error or a break statement). The return() method can be deployed if an object needs to clean up or release resources before completing traversal.

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

In the code above, the function readLinesSync takes a file object as an argument and returns a traverser object with the return() method deployed in addition to the next() method. In both cases, the return() method is triggered.

For (let line of readLinesSync(fileName)) {console.log(line); break; } for (let line of readLinesSync(fileName)) {console.log(line); throw new Error(); }Copy the code

In the code above, after the first line of case 1 output file, the return() method is executed to close the file; Case two throws an error after the return() method closes the file.

Note that the return() method must return an object, as Generator syntax dictates.

The throw() method is used primarily in conjunction with Generator functions and is not used for normal traverser objects. See the chapter Generator Functions.

for… Of circulation

ES6 uses C++, Java, C#, and Python to introduce for… The of loop, as a unified way to traverse all data structures.

A data structure that deploys the Symbol. Iterator attribute is considered to have an iterator interface. The of loop iterates through its members. That is to say, for… Inside the of loop is the symbol. iterator method of the data structure.

for… The scope of the of loop includes arrays, Set and Map structures, some array-like objects (such as Arguments objects, DOM NodeList objects), Generator objects, and strings.

An array of

The iterator interface is native to arrays (i.e. the Symbol. Iterator attribute is deployed by default), for… The of loop is essentially a traverser generated by calling this interface, as evidenced 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
}
Copy the code

In the code above, the empty object obj deploys the Symbol. Iterator property of the array arr, resulting in obj’s for… The of loop produces exactly the same result as ARR.

for… The 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});Copy the code

JavaScript’s original for… In loop, can only get the key name of the object, not the key value directly. ES6 provided for… The of loop, which 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
}
Copy the code

The code above shows that for… In loop reads the key name, for… The of loop reads the key value. If you want to pass for… The “of” loop gets the index of an array using the entries and keys methods of an array instance (see “Extending Arrays”).

for… The of loop calls the traverser interface, which returns only properties with numeric indexes. This has something to do with… The in loop is also different.

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"}Copy the code

In the code above, for… The of loop does not return the foo property of the array arr.

Set and Map structures

The Set and Map structures also have native Iterator interfaces that can be used directly with for… Of circulation.

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
Copy the code

The above code demonstrates how to traverse the Set and Map structures. There are two things to note. First, the traversal is done in the order in which each member is added to the data structure. Second, the Set traversal returns a value, while the Map traversal returns an array whose two members 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
Copy the code

Compute the generated data structure

Some data structures are computationally generated based on existing data structures. For example, ES6 arrays, sets, and maps all deploy the following three methods, which return a traverser object when called.

  • entries()Returns an traverser object for traversal[Key name, key value]Is an array. For arrays, the key is the index value; For a Set, the key name is the same as the key value. The Map Iterator interface is called by defaultentriesMethods.
  • keys()Returns an iterator object that iterates over all key names.
  • values()Returns a traverser object that iterates over all key values.

The traverser objects generated after these three method calls are traversed through the computed data structures.

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

An array-like object

There are several classes of array-like objects. The following is the for… The of loop is used for examples of strings, DOM NodeList objects, 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"); Function printArgs() {for (let x of arguments) {console.log(x); } } printArgs('a', 'b'); // 'a' // 'b'Copy the code

For strings, for… Another feature of the OF loop is that it correctly recognizes 32-bit UTF-16 characters.

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

Not all array-like objects have an Iterator interface, and a handy solution is to use the array. from method to convert them to an Array.

let arrayLike = { length: 2, 0: 'a', 1: 'b' }; For (let x of arrayLike) {console.log(x); } for (let x of array. from(arrayLike)) {console.log(x); }Copy the code

object

For ordinary objects, for… The of structure cannot be used directly because an error is reported. You must deploy the Iterator interface to use it. But in this case, for… The in loop can still be used to iterate over 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
Copy the code

The code above says that for ordinary objects, for… The in loop iterates over key names, for… The of loop will report an error.

One solution is to use the object. keys method to generate an array of the Object’s key names and then iterate over the array.

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

Another option is to rewrap 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
Copy the code

Comparison with other traversal grammars

In the case of arrays, JavaScript provides multiple traversal syntax. The original way to write it is the for loop.

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

This is cumbersome, so arrays provide built-in forEach methods.

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

The problem with this is that you can’t break out of the forEach loop, and neither break nor return will work.

for… The in loop iterates through the key names of a number group.

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

for… The in loop has several disadvantages.

  • The array’s key name is a number, butfor... inLoops are strings with keys “0”, “1”, “2”, and so on.
  • for... inThe loop iterates not only over numeric key names, but also over other manually added keys, and even keys on the prototype chain.
  • In some cases,for... inThe loop iterates over the key names in any order.

All in all, for… The in loop is designed primarily for traversing objects, not for traversing groups of numbers.

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

for (let value of myArray) {
  console.log(value);
}
Copy the code
  • With withfor... inSame concise syntax, but nofor... inThose flaws.
  • Different from theforEachMethod, it can be withbreak,continueandreturnUse together.
  • Provides a unified operation interface for traversing all data structures.

Here is a statement that uses the break statement to break for… An example of an of loop.

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

The example above prints Fibonacci numbers less than or equal to 1000. If the current item is greater than 1000, the for… statement is broken. Of circulation.