1. The Concept and Function of Iterator
The main purpose of iterator is to provide a unified and convenient access and traversal mechanism for collections in JS such as Array, Map, Set or some custom collections. It is actually an interface. Any data structure can be traversed by deploying the iterator interface. ES6 creates a new traversal command for...of loop. The Iterator interface is mainly for...of consumption.
This is how Iterator traverses.
(1) Create a pointer object that points to the starting position of the current data structure. That is to say, traversal object is essentially a pointer object.
(2) The next method of the pointer object is called for the first time, which can point the pointer to the first member of the data structure.
(3) The second call to the next method of the pointer object points to the second member of the data structure.
(4) Call the next method of the pointer object until it points to the end of the data structure.
Each call to the next method returns information about the current member of the data structure. Specifically, it returns an object with two attributes, value and done. The value attribute is the value of the current member, and the done attribute is a Boolean value to indicate whether the traversal ends. In fact, we can simulate an iterator generating function as follows:
function myIterator(array) { let index = 0; return { next: () => { let length = array.length; if(index < length){ return { value: array[index++], done: false, } }else { return { value: undefined, done: true } } } } } let arr = new Array(1,2); let iterator = myIterator(arr); console.log(iterator.next()); //{ value: 1, done: false } console.log(iterator.next()); //{ value: 2, done: false } console.log(iterator.next()); //{ value: undefined, done: true}
2. Default Iterator interface
The purpose of the Iterator interface is to provide a unified access mechanism for all data structures, i.e. for...of loops (see below). When a for...of loop is used to traverse a data structure, the loop automatically looks for the Iterator interface. As long as an Iterator interface is deployed in a data structure, we call it "iterable". ES6 stipulates that the default Iterator interface is deployed in the Symbol.iterator attribute of the data structure, or that a data structure can be considered "iterable" as long as it has the Symbol.iterator attribute. The Symbol.iterator property itself is a function, which is the default traversal generation function for the current data structure. Executing this function returns a traversal. For example, the following ob is traversable.
const obj = { [Symbol.iterator] : function () { return { next: function () { return { value: 1, done: true }; } }; } };
Some data structures in ES6 are native to Iterator interfaces (such as arrays), which can be traversed by for...of loops without any processing. The reason is that these data structures naturally deploy Symbol.iterator attributes (see below), while others do not (such as objects). Any data structure that deploys the Symbol.iterator attribute is called the deployed traversal interface. Calling this interface returns a traversal object. The data structure with Iterator interface is as follows.
For example, a String type:
- Array
- Map
- Set
- String
- TypedArray
- arguments object of function
- NodeList object
let str = '123'; let strIterator = str[Symbol.iterator](); //Symbol.iterator is actually a method that return s an object with a next method. console.log(strIterator.next()); //{ value: '1', done: false } console.log(strIterator.next()); //{ value: '2', done: false } console.log(strIterator.next()); //{ value: '3', done: false } console.log(strIterator.next()); //{ value: undefined, done: true }
In fact, we can modify the default traversal:
String.prototype[Symbol.iterator] = function() { let end = false; return { next: () => { if(!end) { end = !end return {value: 'myIterator', done: false} }else { return {value: undefined, done: true} } } } } let str = '123'; let strIterator = str[Symbol.iterator](); console.log(strIterator.next()); //{ value: 'myIterator', done: false } console.log(strIterator.next()); //{ value: undefined, done: true }
3.for...of Cycle
ES6 uses C++, Java, C# and Python languages for reference, and introduces for...of loops as a unified way to traverse all data structures. Once a data structure deploys the Symbol.iterator attribute, it is considered to have an iterator interface, and its members can be traversed in a for...of loop. That is to say, the Symbol.iterator method of the data structure is invoked inside the for...of loop. for...of loops can use arrays, Set and Map structures, certain array-like objects (such as arguments objects, DOM NodeList objects), Generator objects, and strings. In fact, we can also traverse the object by adding Symbol.iterator method to the object, but it is not necessary, because in this case, it is better to use map directly.
//Map,Set 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); } //perhaps 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
4. Scenarios using the Iterator interface
(1) Deconstruction assignment
Symbol.iterator method is called by default when deconstructing and assigning arrays and sets (note: only arrays and sets)
let set = new Set([ 'a', 'b', 'c' ]) let [x, y] = set; let [a, ...rest] = set; console.log(x, y); //a b console.log(a, rest); //a [b,c] let arr = [1,2,3]; let [x1,y1] = arr; console.log(x1, y1); //1 2
(2) Extension Operators (see Differences between Extension Operators and Deconstruction) Here)
let myarr = [1,2,3]; let newarr = [...myarr,4,5]; console.log(newarr); //[1,2,3,4,5] console.log(myarr); //[1,2,3] let set = new Set().add('a').add('b').add('c'); let setarr = [...set, 1, 2]; console.log(set); //Set { 'a', 'b', 'c' } console.log(setarr); //[ 'a', 'b', 'c', 1, 2 ]
In fact, this provides a simple mechanism for converting any data structure deployed with the Iterator interface to an array. That is, as long as an Iterator interface is deployed to a data structure, it can be converted to an array using an extension operator.
let arr = [...iterable];
(3)yield*
yield* is followed by a traversable structure that calls the traversal interface of that 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
Because traversal of arrays calls the traversal interface, traversal interface is actually called in any situation that accepts arrays as parameters. Here are some examples.
- for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet() (such as new Map (['a', 1], ['b', 2]))
- Promise.all()
- Promise.race()
5. Implementing Symbol.iterator Method with generator Function
let myIterable = { [Symbol.iterator]: function* () { yield 1; yield 2; yield 3; } } [...myIterable] // [1, 2, 3] // Or use the following succinct writing 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 deploys any code, as long as the return value of each step is given with the yield command.
6. return(), throw() of traversal object
In addition to the next method, the traversal object can also have return method and throw method. If you write the traversal object generator function yourself, the next method must be deployed, and whether the return method and throw method are deployed is optional.
The return method is used where the return method is invoked if the for...of loop exits early (usually because of an error or a break statement). If an object needs to clean up or release resources before it completes traversal, the return method can be deployed.
function readLinesSync(file) { return { [Symbol.iterator]() { return { next() { return { done: false }; }, return() { file.close(); return { done: true }; } }; }, }; }
In the code above, the function readLinesSync takes a file object as a parameter and returns a traversal object, in which the return method is deployed in addition to the next method. In both cases, the return method is triggered.
// Situation I 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, the return method is executed after the first line of the output file, closing the file; and the error is thrown after the return method is executed to close the file. Note that the return method must return an object, which is determined by the Generator specification.