introduce

This is the 16th article in JavaScript’s advanced in depth series that covers generators and iterators

The body of the

1. The iterator

1.1 What is an iterator

An iterator is an object that allows the user to navigate over a container (such as a linked list or array) without having to worry about the internal implementation details of the object.

  • Behaving like a cursor in a database, iterators first appeared in the CLU programming language, which was designed in 1974
  • Iterators are implemented differently in various programming languages, but many languages have iterators, such as Java, Python, and so on

By definition, an iterator is an object that helps us traverse a data structure

In JS, an iterator is also a concrete object that conforms to the Iterator Protocol.

  • The iterator protocol defines a standard way to produce a series of values, whether finite or infinite
  • This is a specific one in the JS standardnextmethods

1.2 Next method specification

The next method has the following requirements:

  • A function that takes no arguments or one argument returns an object that should have the following two properties:
  • done(boolean)
    • If the iterator can produce the next value in the sequencefalseEquivalent to nothingdoneThis property)
    • If the iterator has iterated through the sequence, otherwisetrueIn this case,valueIs optional, and is the default value after the iteration if it still exists
  • value
    • Any JS value returned by the iterator,donetrueCan be omitted

1.3 Iterator examples

const names = ['Alex'.'John'.'Alice']

// Create an iterator object to access the array
let index = 0
const namesIterator = {
  next() {
    if (index < names.length) {
      return { done: false.value: names[index++] }
    } else {
      return { done: true.value: undefined}}}},console.log(namesIterator.next()) // { done: false, value: 'Alex' }
console.log(namesIterator.next()) // { done: false, value: 'John' }
console.log(namesIterator.next()) // { done: false, value: 'Alice' }
console.log(namesIterator.next()) // { done: true, value: undefined }
Copy the code

1.4 Iterable

The code above will look strange:

  • When we get the array, we need to create one of our ownindexVariable, and create what’s called an iterable
  • We can actually wrap the above code to make it an iterable

What is an iterable?

  • Iterator and iterator are different concepts
  • When an object is implementediterator protocolIterator protocol, it is an iterable
  • The requirements of this object must be fulfilled@@iteratorMethod, which we use in our codeSymbol.iteratoraccess
const namesIterator = {
  names: ['Alex'.'John'.'Alice'],
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () = > {
        if (index < this.names.length) {
          return { done: false.value: this.names[index++] }
        }
        return { done: true.value: undefined}},}},}Copy the code

1.5 Native iterator objects

Many of the native objects we normally create already implement the iterable protocol and generate an iterator object:

  • String,Array,Map,Set,argumentsObjects,NodeListA collection of
const names = ['Alex'.'John'.'Alice']
const namesIterator = names[Symbol.iterator]()

console.log(namesIterator.next()) // { value: 'Alex', done: false }
console.log(namesIterator.next()) // { value: 'John', done: false }
console.log(namesIterator.next()) // { value: 'Alice', done: false }
console.log(namesIterator.next()) // { value: undefined, done: true }
Copy the code

1.6 Application of iterable

  • Syntax in JS:for... of,A grammar,yield*,Deconstruction assignment
  • When creating some objects:new Map([iterable]),new WeakMap([iterable]),new Set([iterable]),new WeakSet([iterable])
  • Some method calls:Promise.all(iterable),Promise.race(iterable),Array.from(iterable)

1.7 Iterability of custom classes

Objects created by Array, Set, String, Map, etc. are all iterable

  • In object-oriented development, you can also create your own class for creating iterable objects

Example: Create a classroom class

  • Classrooms have their own attributes (name, maximum capacity, current classroom students)
  • Access to new studentspush
  • The classroom objects created are all iterable
class Classroom {
  constructor(name, maxCount, currentStudents) {
    this.name = name
    this.maxCount = maxCount
    this.currentStudents = currentStudents
  }
  push(student) {
    if (this.maxCount === this.currentStudents.length) return
    this.currentStudents.push(student)
  }
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () = > {
        if (index < this.currentStudents.length) {
          return { done: false.value: this.currentStudents[index++] }
        }
        return { done: true.value: undefined}},// Use return to listen for exit operations
      return: () = > {
        console.log('iterator break')
        // Remember to return
        return { done: true.value: undefined}},}}}const c1 = new Classroom('6-1'.40['Lily'.'Alex'.'John'.'Jason'])
for (const student of c1) {
  console.log(student)
  if (student === 'Alex') break
}
// Lily
// Alex
// iterator break
Copy the code

2. The generator

2.1 What is a generator

Generators are a new feature in ES6 that gives us more flexibility in controlling when a function continues, pauses, and so on.

In normal functions, the termination condition is usually a return value or an exception.

A generator function is also a function, but with some differences:

  • First, the generator function needs to be infunctionI’ll put one after that*
  • Second, generator functions can passyieldKeyword to control the execution flow of the function
  • Finally, the return value of the generator function is oneGeneratorGenerators. Generators are a special kind of iterator
function* foo() {
  console.log('Function starts executing')

  const value1 = 100
  console.log(value1)
  yield

  const value2 = 200
  console.log(value2)
  yield

  const value3 = 300
  console.log(value3)
  yield

  console.log('Function completed')}// When a generator function is called, a line of code inside the function is not executed
// Instead, return a generator object
const generator = foo()
Copy the code
// Call each section of code before yield with the next method
generator.next()
// The function starts executing
/ / 100

generator.next()
/ / 200

generator.next()
/ / 300

generator.next()
// The function completes
Copy the code

2.2 Return value of generator function

Iterators can return {done: Boolean, value: “} after executing the next method. Generator functions can also return data during execution

function* foo() {
  yield
  return 'finish'
}

const generator = foo()

console.log(generator.next()) // { value: undefined, done: false }
console.log(generator.next()) // { value: 'finish', done: true }
Copy the code

From this we know that the value of return in the generator function will be the value of done

So how do we control the value of each step? Yield can be followed by a value/expression that will be the value of each step

function* foo() {
  yield 'first'
  yield 'second'
  return 'finish'
}

const generator = foo()

console.log(generator.next()) // { value: 'first', done: false }
console.log(generator.next()) // { value: 'second', done: false }
console.log(generator.next()) // { value: 'finish', done: true }
Copy the code

2.3 Other ways to use generators

1. Next passes parameters

When we call the generator’s next method, we can pass in a parameter, so where will it be received?

function* foo() {
  console.log('First code')
  // Will receive arguments here
  const input = yield
  console.log('input', input)
  console.log('Second code')}const generator = foo()

generator.next()
// The first code
generator.next('input')
/ / input input
// Second code
Copy the code

The arguments we pass in when we call next() will be returned as the last yield of the code.

So how does the first piece of code get arguments? You can pass arguments to generator functions.

function* foo(input) {
  console.log('input', input)
}

const generator = foo('Passed argument')
generator.next() // input Input parameter
Copy the code

2. Generator terminates prematurely – return method

function* foo() {
  console.log('First code')
  const input = yield
  console.log('Second code')
  console.log('input', input)
  yield
  console.log('Third code')}const generator = foo()
generator.next() // The first code
const returnValue = generator.return('Passed argument')
console.log('returnValue', returnValue) // returnValue {value: 'passed in ', done: true}
generator.next() // Will not be executed
Copy the code

From the code above, we can use the return method to prematurely interrupt the execution of the generator

const input = yield
return input  // just add a line of code to return input
Copy the code

3. The generator throws an exception-throw method

function* foo() {
  console.log('First code')
  yield
  console.log('Second code')
  yield
  console.log('Third code')}const generator = foo()
generator.next() // The first code
generator.throw() // With this code, an error is reported directly
generator.next() // Will not be executed
Copy the code

From the code above, we can use the return method to prematurely interrupt the generator’s execution, equivalent to throwing an exception at the first yield that needs to be caught to execute the code below

function* foo() {
  console.log('First code')
  // Catch an exception here
  try {
    yield
  } catch (error) {}
  console.log('Second code')
  yield
  console.log('Third code')}const generator = foo()
generator.next() // The first code
generator.throw() // Second code
generator.next() // The third code
// It can be executed normally
Copy the code

The throw method can also pass parameters that are received by the catch parameter

2.4 Generators replace iterators

Generators are a special type of iterator, so they can be used directly in place of iterators in certain application scenarios

// Iterator code
const namesIterator = {
  names: ['Alex'.'John'.'Alice'],
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () = > {
        if (index < this.names.length) {
          return { done: false.value: this.names[index++] }
        }
        return { done: true.value: undefined}},}},}Copy the code
// Generator code
const names = ['Alex'.'John'.'Alice']
function* generator(names) {
  for (const item of names) {
    yield item
  }
}

const gen = generator(names)

console.log(gen.next()) // { value: 'Alex', done: false }
console.log(gen.next()) // { value: 'John', done: false }
console.log(gen.next()) // { value: 'Alice', done: false }
console.log(gen.next()) // { value: undefined, done: true }
Copy the code

We can actually produce an iterable by yield* (this is the yield syntactic sugar, but iterates over the iterable one value at a time)

/ / using yield *
const names = ['Alex'.'John'.'Alice']
function* generator(names) {
  yield* names
}

const gen = generator(names)

console.log(gen.next()) // { value: 'Alex', done: false }
console.log(gen.next()) // { value: 'John', done: false }
console.log(gen.next()) // { value: 'Alice', done: false }
console.log(gen.next()) // { value: undefined, done: true }
Copy the code

2.5 Generator schemes for custom classes

We can use generators to rewrite the 1.7 case

class Classroom {
  constructor(name, maxCount, currentStudents) {
    this.name = name
    this.maxCount = maxCount
    this.currentStudents = currentStudents
  }
  push(student) {
    if (this.maxCount === this.currentStudents.length) return
    this.currentStudents.push(student)
  }
  // If the function keyword is not available, it can be written before the method name* [Symbol.iterator]() {
    yield* this.currentStudents
  }
}

const c1 = new Classroom('6-1'.40['Lily'.'Alex'.'John'.'Jason'])
for (const student of c1) {
  console.log(student)
}
// Lily
// Alex
// John
// Jason
Copy the code

3. Promise flaws

In Article 15, we introduced Promise as a specification for asynchronous code. Is Promise really that good? Admittedly, it’s very user-friendly, but there are situations where nested hell can occur:

Requirement: The interface needs to be requested multiple times, and the data returned by multiple interfaces is combined to print the final data

// requestData returns a Promise
requestData("alex").then(res= > {
    requestData(res + 'bbb').then(res2= > {
        requestData(res + 'ccc').then(res3= > {
            requestData(res3 + 'ddd').then(res4= > {
                / /...})})})})Copy the code

If the above scenario occurs, there may be other complex business logic between multiple nesting, then there is still callback hell, so how to solve it?

3.1 Solution 1

requestData("alex").then(res= > {
    return requestData(res + 'aaa')
}).then(res= > {
    return requestData(res + 'bbb')
}).then(res= > {
    return requestData(res + 'ccc')
}).then(res= > {
    / /...
})
Copy the code

This way, the nesting problem can be solved, but the readability is still very poor, and it can look complicated when mixed with logical code

3.2 Solution 2

For this solution, we decided to use Promise + Generator

function* getData(params) {
  const params1 = yield requestData(params)
  const params2 = yield requestData(params1)
  const params3 = yield requestData(params2)
  const params4 = yield requestData(params3)
  console.log(params4)
}

const generator = getData('alex')
// Write a function that automatically executes the generator
function execGeneratorFn(genFn) {
  const generator = genFn()
  function exec(params) {
    const result = generator.next(params)
    if (result.done) {
      return result.value
    }
    result.value.then(res= > {
      exec(res)
    })
  }
  exec()
}

execGeneratorFn(getData)
Copy the code

3.3 Solution 3

The code for the auto-execute generator we implemented above already has a library of CO in the community

npm install co
Copy the code
const co = require('co')
co(getData)  // This approach also works
Copy the code

3.4 use the async/await

Since ES8, JS supports automatic execution of generators

async function getData() {
  const params1 = await requestData('alex')
  const params2 = await requestData(params1 + 'aaa')
  const params3 = await requestData(params2 + 'bbb')
  const params4 = await requestData(params3 + 'ccc')
  console.log(params4)
}

getData()  / / no problem
Copy the code

So we can see that async/await is essentially a syntactic candy of Promise + Generator

conclusion

This article explains what generators and iterators are and demonstrates them with several examples. Also, async/await is finally elicited via generator + Promise

In the next article, we will focus on async, await, and event loops