Introduction to iterators

An Iterator is an Iterator. It is found in many programming languages. JavaScript formally defines a standardized interface for iterators in the ES6 specification.

Why do we need iterators

This question? It’s time to start with design patterns.

We know that there is an iterator pattern in design patterns. The iterator pattern solves the problem of traversing different collections (arrays, maps, sets, etc.) in different ways, and rewriting the code for each collection. Trouble! Lazy programmers wonder: Could there be a universal way to traverse collection elements?

Thus, the iterator pattern was born. It is a mechanism to implement uniform traversal operations on different sets. It can also be understood as an abstraction of the set traversal behavior.

In the world of happyEnding, as soon as you implement the iterator interface, you join the iterator family. The function next() is a pass.

Iterators in ES6

In ES6, what is considered an object that provides iterators? Two conditions must be met:

  • A function that implements the Iterable interface and must be able to generate iterator objects;
  • An iterator Object containing the next () method, next () function returns the format is: {value | Object, done | Boolean}.

Let’s take a look at chestnuts:

class Users {
    constructor(users){
        this.users = users;
    }

    // Implement Iterable interface
    [Symbol.iterator]: function(){
	let i =0;
	let users = this.users;

        // Return an iterator
	return {

            // Must include the next() method
            / / the return value format compliance: {value | Object, done | Boolean}
	   next(){
	          if( i < users.length){
		  	return {done:false.value:users[i++]};
		  }
		  return {done : true}; }}}}Copy the code

This chestnut is ES6 compliant. It is worth noting here that we use a special Symbol value defined in ES6, symbol.iterator. Any Object can customize the implementation of the iterator by adding this property. Symbol is not the focus of this article.

Let’s see how we can use this object that provides iterators:

const allUsers = new Users([
	{name: 'frank'},
	{name: 'niuniu'},
	{name: 'niuniu2'}]);// Verify mode 1: ES6 for... Of actively calls symbol.iterator
for( let v of allUsers){
	console.log( v );
}
//output:: ${name: 'frank' }
  { name: 'niuniu' }
  { name: 'niuniu2' }

// Verification method 2: call it yourself

// Actively returns an iterator object
const allUsersIterator = allUsers[Symbol.iterator]();

console.log(allUsersIterator.next());
console.log(allUsersIterator.next());
console.log(allUsersIterator.next());
console.log(allUsersIterator.next());
//output:: ${done: false.value: { name: 'frank'}} {done: false.value: { name: 'niuniu'}} {done: false.value: { name: 'niuniu2'}} {done: true }
Copy the code

To recap: The Array, Map, and Set built-in objects in ES6 all implement symbol. iterator; in short, they implement iterator properties. However, to explicitly use the iterator properties of the corresponding object, you need to call it yourself:

letThe bar = [1, 2, 3, 4]; // Call the build iterator explicitlyletbarIterator = bar[Symbol.iterator](); // Use the iterator feature console.log(bariterator.next ().value); // output : 1Copy the code

From iterators to generators

Having said Iterator, let’s talk about the Generator. Why do you need a Generator in JavaScript?

There are two explanations:

  • To make iterators work more elegantly, we use generators for a layer of wrapping
  • To address the multiple layers of promises in ES5thenIt’s not intuitive

Take a look at how the User object behaves under the generator above:

class Users {
	constructor(users){
		this.users = users;
		this.length = users.length;
	}

	*getIterator(){
		for( let i=0; i< this.length; i++ ){
			yield this.users[i]; }}}const allUsers = new Users([
	{name: 'frank'},
	{name: 'niuniu'},
	{name: 'niuniu2'}]);/ / verification
let allUsersIterator = allUsers.getIterator();
console.log(allUsersIterator.next()); //{ value: { name: 'frank' }, done: false }

Copy the code

Doesn’t it look a little easier? If this were just to make iterators look more elegant, ES6 wouldn’t need generators as a new functional form at all. Generators have more complex syntax. This complexity exists to accommodate a wider range of application scenarios.

Let’s look at the generator syntax first:

The statement

Generate generator functions by using the following syntax:

function *foo(){
	/ /...
}
Copy the code

Even though the generator uses * to declare it, it still behaves like a normal function:

foo();
Copy the code

You can also pass it a reference:

function*foo(x,y){ //... } foo (24, 2);Copy the code

The main difference is that executing a generator, such as foo(24,2), does not actually execute code in the generator. Instead, it generates an iterator that controls the generator to execute its code. (The process of executing a generator function to generate an iterator is similar to the process shown earlier in this article.)

For the code to work, you call the iterator method next().

function *foo(){
	/ /...
}
// Generate iterators
let fooIterator = foo();

/ / execution
fooIterator.next();
Copy the code

In case you’re wondering, how does the function know what to return when we call the iterator method next()? There is no return in a generator function. Don’t worry, the yield keyword plays this role.

yield

The yield keyword is used in generators to indicate a pause point. Look at the following code:

function *foo() {let x=10;
	let y=20;
	yield;
	let z = 10 + 20;
}
Copy the code

In this generator, the first two lines are run and yield pauses the generator. If resumed, it is executed at yield. That’s it. It pauses whenever it hits yield. The yield can occur any number of times in a generator, and you can even put it in a loop.

Yield is not just a pause point; it is an expression. To the right of yield, is the value returned when paused (that is, when the iterator is called next()). While yield is at the statement position, you can also insert input parameters in the next() method (replacing yield and the expression to the right) :

function *foo(){
	let x=10;
	let y=20;
	let z = yield x+y;
	return x + y +z;
}
// Generate iterators
let fooIterator = foo();
The first time we execute next() of the iterator, yield returns the result of the run to the right of yield
console.log(fooIterator.next().value); / / 30
When next(100) of the iterator is executed the second time, the position of yield and the expression to the right of the iterator is replaced by the argument 100
console.log(fooIterator.next(100).value); / / 130
Copy the code

Incorporate promises to control asynchronous operations

ES5 promises include asynchronous actions that return a resolution when the action is complete. But its writing then() makes code ugly in complex cases, and numerous THEN nesting doesn’t look much better than callback hell. So we wondered if we could use generators to better control the asynchronous operation of promises.

The Promise is put after yield, and the iterator listens for the Promise’s resolution (complete or reject), and then either restores the generator’s permission with the completion message (calling next()), or throws an error to the generator with the reason for the rejection.

This is the most important point: yield a Promise, and then use that Promise to control the iteration of the generator.

import 'axios';

// Step 1: Define the generator
function *main(){
    try {
        var text = yield myPromise();
        console.log("generator result :", text);
    }catch(err){
        console.error("generator err :",err); }}// Step 2: Define the Promise function
function myPromise(){
    return axios.get("/api/info");
}

// Create an iterator
let mainIterator = main();

// Step 4: Use promise to control how the iterator works
let p = mainIterator.next().value;

p.then(
    function(res){
        let data = res.data;
        console.log(" resolved : ", data);
        / /! The promise resolves (completes) to control the iterator
        mainIterator.next(data);
    },
    function(error){
        console.log(" rejected : ", error);
        / /! The promise resolution (reject) controls the iteratormainIterator.throw(error); });//output
$ resolved : {name : frank}
  generator result : {name : frank}
Copy the code

Thus, we saw how to use promises in generators. And can work well. It’s just that, as you’ll see, this code is even more verbose than the old Promise.

If you have a library that encapsulates all the details of the combination of generators, iterators, and promises, you can switch from asynchronous to synchronous writing with a simple call (just writing steps 1 and 2 above). Do you think either?

ES7 async/await

The above description is a prototype of the ASYNc /await syntax in ES7. Its implementation and considerations are far more complex than our demo version above. It is written as follows:

function myPromise() {return axios.get("/api/info");
}

async main(){ try { var text = await myPromise(); console.log(text); }catch(err){ console.log(err); }}Copy the code

If you contrast the async/await syntax with generators, you can simply analogy async with * and await with yield. It is the official implementation that incorporates Promise in generators and iterators.

More about async/await syntax in ES7 is not the focus of this article. Knowing the ins and outs is what I care about. So that’s the end of the chapter!

summary

This is the main content of the article. We moved from iterators to generators, and eventually with promises led to the async/await syntax in ES7. Hope to help!


JavaScript in-depth series:

“var a=1;” What’s going on in JS?

Why does 24. ToString report an error?

Here’s everything you need to know about “JavaScript scope.

There are a lot of things you don’t know about “this” in JS

JavaScript is an object-oriented language. Who is for it and who is against it?

Deep and shallow copy in JavaScript

JavaScript and the Event Loop

From Iterator to Async/Await

Explore the detailed implementation of JavaScript Promises