从迭代器、生成器到async、await

940 阅读8分钟

认识迭代器与生成器

迭代器

迭代器是帮助我们对某个数据结构进行遍历的对象

JS中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(irerator protocol

  • 迭代器协议定义了一系列值(无论是优先还是无限个)的标准方式
  • js中这个标准就是一个特定的next方法

next方法有以下要求:

  • 一个无参函数或一个参数,返回一个应当拥有一些两个属性对象(MDN文档)
  • done(boolean)
    • 如果迭代器可以产生序列中的下一个值,则为false(这等价于没有指定done这个属性)
    • 如果迭代器已将序列迭代完毕,则为true,这种情况下,value是可选的,即为迭代结束后的默认返回值
  • value
    • 迭代器返回的任何JS值,donetrue时可以省略

例子

function createArrayIterator(arr) {
  let index = 0
  return {
    next: function () {
      if (index < arr.length) {
        return { done: false, value: arr[index++] }
      } else {
        return { done: true, value: undefined }
      }
    }
  }
}

// 数组
const names = ['abc', 'cba', 'nba']

const namesIterator = createArrayIterator(names)

console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());

可迭代对象

可迭代对象和迭代器是不同的概念,当一个对象实现了iterable protocol协议时,他就是一个可迭代对象,这个对象的要求是必须实现迭代器的方法(iterator),在代码中我们可以使用Symbol.iterator访问该对象

那么可迭代对象用在哪里呢?

  • JS中的语法:for of、展开语法(spread syntax)、yield*、解构赋值(Destructuring_assignment
  • 创建一些对象:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable])
  • 一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable)

认识for of

for of遍历的必须是可迭代对象,而且它是根据可迭代对象里面的迭代器中的done来决定是否停止的,如果donetrue则停止遍历

const iterableObj = {
  names: ['abc', 'cba', 'nba'],
  [Symbol.iterator]: function () {
    let index = 0
    return {
      next: () => {
        if (index < this.names.length) {
          return { done: false, value: this.names[index++] }
        } else {
          return { done: true, value: undefined }
        }
      }
    }
  }
}

// iterableObj对象就是一个可迭代对象
// console.log(iterableObj[Symbol.iterator]);

const iterator = iterableObj[Symbol.iterator]()
// console.log(iterator.next());
// console.log(iterator.next());
// console.log(iterator.next());
// console.log(iterator.next());

// for of遍历的必须是可迭代对象,所以下面的obj是不能使用for of遍历的
// const obj = {
//   name: 'zs',
//   age: 18
// }

for (const item of iterableObj) {
  console.log(item);
}

内置可迭代对象

事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的,如:

  • String、Array、Map、Set、arguments对象、NodeList集合、展开运算符、解构等

注意:

对象并不是可迭代对象,所以在使用展开运算符展开对象时也不是可迭代对象,而是在ES9(2018)中新增的一个特性,解构对象的时候也是ES9新增的

如下例子:

1.数组其实是通过new Array()的类来创建出来的可迭代对象,我们可以打印来看一下

const names = ['abc', 'cba', 'nba']
console.log(names[Symbol.iterator]);	// [Function: values]
console.log(Object.getOwnPropertyDescriptors(Array))

const iterator1 = names[Symbol.iterator]()
console.log(iterator1.next());
console.log(iterator1.next());
console.log(iterator1.next());
console.log(iterator1.next());

for (const item of names) {
  console.log(item);
}

2.MapSet都是可迭代对象

const set = new Set()
set.add(10)
set.add(20)
set.add(30)

console.log(set[Symbol.iterator]);

for (const item of set) {
  console.log(item);
}

自定义可迭代类

在类中创建一个迭代器([Symbol.iterator])即可,在迭代器中定义return函数可以监听迭代器什么时候停掉了

class Classroom {
  constructor(address, name, students) {
    this.address = address
    this.name = name
    this.students = students
  }

  entry(newStudent) {
    this.students.push(newStudent)
  }

  [Symbol.iterator]() {
    let index = 0
    return {
      next: () => {
        if (index < this.students.length) {
          return { done: false, value: this.students[index++] }
        } else {
          return { done: true, value: undefined }
        }
      },
      return: () => {
        console.log('迭代器提前停止了');
        // return函数也是需要return的
        return { done: true, value: undefined }
      }
    }
  }
}

const classroom = new Classroom('1栋101', '计算机教室', ['zs', 'ls'])
classroom.entry('ww')

for (const stu of classroom) {
  console.log(stu);
  // 可使用return来监听迭代器什么时候停掉了
  if (stu === 'ls') break
}

如果是一个函数,我们怎么把它变成可迭代函数呢?

function Person() {

}

Person.prototype[Symbol.iterator] = function () { }

生成器

生成器是ES6中新增的一直函数控制、使用的方案,它可以让我们更加灵活地控制函数什么时候继续执行、暂停执行等

平时我们的函数终止的条件通常是返回值或者发生了异常

要了解生成器,我们需要先了解生成器函数,生成器函数与普通函数的区别:

  1. 生成器函数需要在function的后面添加一个符号*
  2. 生成器函数可以通过yield关键字来控制函数的执行流程
  3. 生成器函数的返回值是一个Generator(生成器)
    • 生成器事实上是一种特殊的迭代器(MDN

例子

// 当生成器函数遇到yield的时候暂停函数执行,遇到return的时候停止函数执行
function* foo() {
  console.log('函数开始执行');

  const value1 = 100
  console.log('第一段代码:', value1);
  yield value1
  // return value1

  console.log('第二段代码:', value2);
  yield	value2 = 200

  console.log('函数执行结束');
  return '123'
}

// 生成器函数返回一个生成器对象generator foo函数里面的代码不会执行
const generator = foo()
console.log(generator); // Object [Generator] {}

// 如果我们想要执行生成器函数里面的代码 必须需要使用next
generator.next()
generator.next()

// generator.next()是由返回值的
console.log('返回值1:', generator.next());	// 返回值1: { value: 100, done: false }
console.log('返回值2:', generator.next());	// 返回值2: { value: undefined, done: false }
console.log('返回值3:', generator.next()); // 返回值3: { value: undefined, done: true } 后面有return 返回值3: { value: '123', done: true }

传递参数

next中的参数传递给yield,生成器函数的参数传递给函数全局

function* foo(num) {
  console.log('函数开始执行');

  const value1 = 100 * num
  console.log('第一段代码:', value1);
  const n = yield value1

  const value2 = 200 * n
  console.log('第二段代码:', value2);
  const count = yield value2

  const value3 = 300 * count
  console.log('第三段代码:', value3);
  yield value3

  console.log('函数执行结束');
  return '123'
}

// 参数传递给第一段代码
const generator = foo(5)

console.log('返回值1:', generator.next());

// 参数传递给第一个yield作为返回值,与yield后面的value1没有关系,yield value只是让{ value: undefined, done: false }里的value有值而已
console.log('返回值2:', generator.next(10));
// 参数传递给第二个yield作为返回值
console.log('返回值3:', generator.next(25)); 

return

generator.return(n)相当于在第一个yield后面添加了return n

function* foo(num) {
  console.log('函数开始执行');

  const value1 = 100 * num
  console.log('第一段代码:', value1);
  const n = yield value1

  const value2 = 200 * n
  console.log('第二段代码:', value2);
  const count = yield value2

  const value3 = 300 * count
  console.log('第三段代码:', value3);
  yield value3

  console.log('函数执行结束');
  return '123'
}

// 参数传递给第一段代码
const generator = foo(5)

console.log('返回值1:', generator.next());
// { value: 15, done: true } 相当于在第一个yield后面添加了return n
console.log('返回值2:', generator.return(15));
console.log('返回值3:', generator.next()); // 返回值3: { value: undefined, done: true }

throw

function* foo(num) {
  console.log('函数开始执行');

  const value1 = 100 * num
  console.log('第一段代码:', value1);
  // 如果不捕获异常 会直接崩掉
  try {
    yield value1
  } catch (err) {
    console.log('捕获到异常:', err);
    // 如果在catch的时候没有yield回去,就会继续往下执行,直到下一个yield的值来当作generator.throw的返回值
    yield 'aaa'
  }

  const value2 = 200
  console.log('第二段代码:', value2);
  const count = yield value2

  console.log('函数执行结束');
  return '123'
}

// 参数传递给第一段代码
const generator = foo(5)

const result = generator.next()
if (result.value !== 200) {
  console.log(generator.throw('err message'));  // 捕获到异常: err message
}

生成器替代迭代器

原本的迭代器写法代码比较多,我们可以使用生成器来替代迭代器,如下例子:

例一:

function* createArrayIterator(arr) {
  // let index = 0
  // 原本的迭代器写法
  // return {
  //   next: function () {
  //     if (index < arr.length) {
  //       return { done: false, value: arr[index++] }
  //     } else {
  //       return { done: true, value: undefined }
  //     }
  //   }
  // }

  // 第一种生成器的写法
  // yield arr[index++]  // { value: 'abc', done: false }
  // yield arr[index++]  // { value: 'nba', done: false }
  // yield arr[index++]  // { value: 'cba', done: false } 

  // 第二种生成器的写法
   // for (const item of arr) {
  //   yield item
  // }

  // 第三种写法 yield* + 可迭代对象 
  // 事实上就是第二种写法的语法糖
  yield* arr
}

const names = ['abc', 'nba', 'cba']
const namesIterator = createArrayIterator(names)
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());

例二

// 创建一个可迭代一个范围内数字的函数
function* createRangeIterator(start, end) {
  let index = start
  // return {
  //   next: function () {
  //     if (index < end) {
  //       return { done: false, value: index++ }
  //     } else {
  //       return { done: true, value: undefined }
  //     }
  //   }
  // }

  while (index < end) {
    yield index++
  }
}

const rangeIterator = createRangeIterator(10, 20)
console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());

例三:使用生成器重构之前的自定义可迭代类

class Classroom {
  constructor(address, name, students) {
    this.address = address
    this.name = name
    this.students = students
  }

  entry(newStudent) {
    this.students.push(newStudent)
  }

  // [Symbol.iterator] = function* () {
  //   yield* this.students
  // }

  *[Symbol.iterator]() {
    yield* this.students
  }
}

const classroom = new Classroom('1栋101', '计算机教室', ['zs', 'ls'])
for (const stu of classroom) {
  console.log(stu);
}

异步代码的处理方案

需求

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(url)
    }, 2000)
  })
}

// 需求:
// 1.zs -> res: zs
// 2.res + 'aaa' -> res: zsaaa
// 1.res + 'bbb' -> res: zsaaabbb

第一种方案

requestData('zs').then(res => {
  requestData(res + 'aaa').then(res => {
    requestData(res + 'bbb').then(res => {
      console.log(res);
    })
  })
})

第二种方案

requestData('zs').then(res => {
  return res + 'aaa'
}).then(res => {
  return res + 'bbb'
}).then(res => {
  console.log(res);
})

第三种方案:Promise + generator 实现

function* getData() {
  const res1 = yield requestData('zs')
  const res2 = yield requestData(res1 + 'aaa')
  const res3 = yield requestData(res2 + 'bbb')
  console.log(res3);
}

// 手动执行生成器函数
// const generator = getData()
// generator.next().value.then(res => {
//   generator.next(res).value.then(res => {
//     generator.next(res).value.then(res => {
//       generator.next(res)
//     })
//   })
// })

// genFn是一个生成器函数
function execGenerator(genFn) {
  const generator = genFn()

  function exec(res) {
    const result = generator.next(res)
    if (result.done) {
      return result.value
    }

    result.value.then(res => {
      exec(res)
    })
  }

  exec()
}

execGenerator(getData)

第四种方案:使用co第三方包,其实就是第三种方案

npm i co
function* getData() {
  const res1 = yield requestData('zs')
  const res2 = yield requestData(res1 + 'aaa')
  const res3 = yield requestData(res2 + 'bbb')
  console.log(res3);
}

const co = require('co')
co(getData)

第五种方案: 使用await、async,这种方案的本质就是第三种方案,所以async在这里类似于function*

async function getData() {
  const res1 = await requestData('zs')
  const res2 = await requestData(res1 + 'aaa')
  const res3 = await requestData(res2 + 'aaa')
  console.log(res3);
}

getData()

异步函数async

async关键字用于声明一个异步函数,返回一个Promise对象:

  • asyncasynchronous单词的缩写,异步、非同步
  • syncsynchronous单词的缩写,同步、同时

基本使用,平常使用的执行顺序与普通函数是一样的

默认返回undefinedPromise.resolve(undefine)

async function foo() {
  console.log('start');
  console.log('中间代码');
  console.log('end');

  // 1.默认返回undefined,也可以自定义 返回的值会作为resolve的参数 
  // return 'aaa'

  // 2.返回thenable
  // return {
  //   then: function (resolve, reject) {
  //     resolve('bbb')
  //   }
  // }

  // 3.返回一个新的promise
  return new Promise((resolve, reject) => {
    resolve('ccc')
  })
}

const promise = foo()
promise.then(res => {
  console.log('exec:', res);
})

抛出异常,在async异步函数中,抛出异常后,还会执行后续代码,再抛出异常,而且抛出的异常会被作为异步函数返回到Promisereject

async function foo() {
  console.log('start');
  console.log('中间代码');
  throw new Error('err message')
}

foo().catch(err => {
  console.log('err:', err);
})

console.log('后续代码');

await关键字

await特点:

  • 通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise
  • 那么await会等到Promise的状态变成fulfilled状态后,再继续执行异步函数

await基本使用

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(url)
    }, 2000)
  })
}

async function foo() {
  // 里面类似于是通过then来获取到requestData的参数,再到then里是吗使用generator.next(参数)传递给yeild返回给res1
  const res1 = await requestData('zs')
  console.log('res1:', res1);

  const res2 = await requestData('aaa')
  console.log('res2:', res2);
}

foo()

await跟上其他的值 await 后面的代码相当于放到了then中执行

async function foo() {
  // 1.普通值
  // const res1 = await 123
  // await 后面的代码相当于放到了then中执行
  console.log('res1')

  // 2.thenable
  // const res2 = await {
  //   then: function (resolve, reject) {
  //     resolve('aaa')
  //   }
  // }

  // 3.new Promise
  const res3 = await new Promise((resolve, reject) => {
    resolve('bbb')
  })

  console.log(res3);
}

foo()

await等待rejectreject会作为异步函数(如下async foo)返回的Promsiereject来使用

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(url)
    }, 2000)
  })
}

async function foo() {
  const res1 = await requestData('zs')
  console.log('res1:', res1);
}

foo().catch(err => {
  console.log('err:', err);
})