迭代器、生成器详解🔥🔥🔥

1,367 阅读22分钟

序言

什么是 迭代器? 广义上来讲 迭代器 (iterator) 有时又称光标 (cursor) 是程序设计软件的一种 设计模式, 可在 容器对象 (container, 例如例如列表、元组或字典) 上遍访的接口, 开发者无需关心容器对象的实现细节, 就可以通过它按照 特定的顺序 访问其中的每个元素, 同时各种语言实现 迭代器 的方式皆不尽同。

一、迭代器协议

JS迭代器协议 定义了一种产生一系列值的 标准方式, 这一系列的值可以是有限个也可以是无限个, 当值是有限个时, 所有的值都被迭代完毕后, 就会返回一个默认返回值(undefined)。

1.1 协议的基本约定

只要一个 对象 实现了以下功能的 next() 方法, 这个 对象 就被称为 迭代器

  1. 无参数或者接受一个参数
  2. 返回一个符合 IteratorResult 接口的对象, 该对象必须有以下属性:
  • done (可选): 布尔值, 表示是否迭代完毕
  • value (可选): 本次迭代的值, 可以是任何值

如下代码: obj 实现了符合上文提到的约定, 那么该对象就能称之为 迭代器, 其中 next() 不接收参数, 每次执行都会 按照顺序 返回 特定的值, 直到迭代结束

const obj = {
  i: 0,
  next(){
    if (this.i < 3) {
      return {
        done: false,
        value: 2 * [this.i++],
      }
    }
    return { done: true, value: undefined }
  }
}

console.log('1', obj.next()); // 1 { done: false, value: 0 }
console.log('2', obj.next()); // 2 { done: false, value: 2 }
console.log('3', obj.next()); // 3 { done: false, value: 4 }
console.log('4', obj.next()); // 4 { done: true, value: undefined }

实际上, IteratorResult 接口中, donevalue 都不是严格要求的, 如果返回没有任何属性的对象, 实际上等价于 { done: false, value: undefined }

const obj = {
  i: 0,
  next(){
    if (this.i < 3) {
      this.i++
      return {}  // 返回空对象, 是被允许的, 等价于 { value: undefined, done: false }
    }
    return { done: true, value: undefined }
  }
}

console.log('1', obj.next()); // 1 {}
console.log('2', obj.next()); // 2 {}
console.log('3', obj.next()); // 3 {}
console.log('4', obj.next()); // 4 { value: undefined, done: true }

总结: 迭代器 其实本质上就是个对象, 只是它实现了特定的协议(约定), 让它能够被叫做 迭代器, 迭代器 其实就一种 设计模式, 它在 JS 中的表现形式就是一个对象定义了一个 next() 方法, 方法返回一个具有 valuedone 属性的对象

1.2 return 方法

协议规定: 迭代器 允许定义一个 return() 方法, 该方法是可选的, 用于提前结束迭代, 当我们调用该方法时, 将会告诉迭代器, 调用者已经完成了迭代, 该方法约定如下:

  1. 无参数或者接受一个参数 value, 通常 value 作为 IteratorResult 接口对象的 value 值进行返回
  2. 返回一个符合 IteratorResult 接口的对象, 该对象必须有以下属性:
  • done (可选): 布尔值, 表示是否迭代完毕;
  • value (可选): 本次迭代的值, 可以是任何值;

如下代码: 实现了 return() 方法, 方法的 value 参数将作为 IteratorResult 接口对象的 value 值, 并且 done 将被设置为 true 表示迭代结束

const obj = {
  i: 0,
  next(){
    if (this.i < 3) {
      return {
        value: 2 * [this.i++],
        done: false,
      }
    }
    return { value: undefined, done: true }
  },
  return(value){
    this.i = 3 // 迫使, 迭代结束 (再次执行 next 不会继续迭代)
    return { value, done: true }
  }
}

console.log('1', obj.next()); // 1 { value: 0, done: false }
console.log('2', obj.return(10)); // 2 { value: 10, done: true }, 提前结束迭代
console.log('3', obj.next()); // 3 { value: undefined, done: true }

1.3 throw 方法

协议规定: 迭代器 允许定义一个 throw() 方法, 该方法是可选的, 用于提前结束迭代, 当我们调用该方法时, 表明 迭代器 的调用者监测到错误, 强制结束迭代, 该方法约定如下:

  1. 无参数或者接受一个参数 exception, 并且 exception 通常是一个 Error 实例
  2. 返回一个符合 IteratorResult 接口的对象, 该对象必须有以下属性:
  • done (可选): 布尔值, 表示是否迭代完毕
  • value (可选): 本次迭代的值, 可以是任何值

如下代码: 实现了 throw() 方法, 参数为 Error 实例, 方法返回IteratorResult 接口对象, 对象中 donetrue 表示迭代结束

const obj = {
  i: 0,
  next(){
    if (this.i < 3) {
      return {
        value: 2 * [this.i++],
        done: false,
      }
    }
    return { value: undefined, done: true }
  },
  throw(exception){
    this.i = 3 // 迫使, 迭代结束 (再次执行 next 不会继续迭代)
    return { value: undefined, done: true }
  }
}

console.log('1', obj.next()); // 1 { value: 0, done: false }
console.log('2', obj.throw(new Error('111'))); // 2 { value: undefined, done: true }, 发生错误, 提前结束迭代
console.log('3', obj.next()); // 3 { value: undefined, done: true }

二、可迭代协议

可迭代协议: 在 ES6 中, 允许在对象中通过 Symbol.iterator 属性来定义或定制对象的 迭代行为, Symbol.iterator 是一个方法, 该方法返回一个 迭代器, 也只有实现了该协议(规定)的对象才能够被 for...of 给循环遍历

2.1 可迭代对象

实现了 可迭代协议 的对象则被称为 可迭代对象, 简单来说: 要成为 可迭代对象, 该对象必须实现 Symbol.iterator 方法, 并且该方法是一个无参数的函数, 其返回值是一个 迭代器

如下代码: 对象 obj 定义了 Symbol.iterator 方法, 该方法返回一个 迭代器, 那么 obj 就被称为 可迭代对象

const obj = {
  [Symbol.iterator](){
    let i = 0;
    return {
      next(){
        if (i < 3) {
          return { value: i++, done: false }
        }
        return { value: undefined, done: true }
      }
    }
  }
}

2.2 循环可迭代对象

上文我们知道了如何实现一个 可迭代对象, 这里如果我们使用 for of 来循环 可迭代对象, 在执行 for...of 循环时, 会发生哪些事呢?

  1. 先调用对象的 Symbol.iterator 方法, 生成一个 迭代器

  2. 循环调用 迭代器next() 方法, 方法会返回具有 valuedone 两个属性的对象, value 表示当前迭代的值, done 则表示是否遍历结束。

  3. 每次判断 done 是否为 true, 如果是则循环会自动结束

下面是演示代码: 方法 forOf 模拟了循环 可迭代对象 的流程, 并且打印出了每次迭代的值

// 可迭代对象
const obj = {
  [Symbol.iterator](){
    let i = 0;
    return {
      next(){
        if (i < 3) {
          return { value: i++, done: false }
        }
        return { value: undefined, done: true }
      }
    }
  }
}

// 自定义方法: 循环打印迭代对象的值
const forOf = (obj) => {
  const iterator = obj[Symbol.iterator]()
  let done = false

  while (!done) {
    const current = iterator.next()
    done = current.done
    if (!done){
      console.log('forOf', current.value) 
    }
  }
}

forOf(obj) // 打印: forOf 0、forOf 1、forOf 2

我们如果使用 for of 来循环迭代对象, 会发现和上面打印结果是一样样滴

const obj = {
  [Symbol.iterator](){
    let i = 0;
    return {
      next(){
        if (i < 3) {
          return { value: i++, done: false }
        }
        return { value: undefined, done: true }
      }
    }
  }
}

for (const value of obj) {
  console.log('forOf', value)  // // 打印: forOf 0、forOf 1、forOf 2
}

其实 for...of 只能用于循环 可迭代对象, 当然除了 for...of 下面这些语法、方法也都必须要求操作对象是一个 可迭代对象

  • for...of

  • 展开语法: const arr = [...rest];

  • 解构语法: const [a, b, c] = arr;

  • Array.from()

  • Map()

  • WeakMap()

  • Set()

  • WeakSet()

  • Promise.all()

  • Promise.allSettled()

  • Promise.race()

  • Promise.any()

  • Array.from()

  • ....

2.3 篡改迭代器

已知, 我们可以使用 for...of 循环数组, 但是不能循环 普通对象, 循环 普通对象 将会提示对象是不可迭代的, 如下代码: for...of 能够正常循环数组、但是不能循环普通对象 obj

const arr = [1, 2, 3]

const obj = {
  name: 'moyuanjiun'
}
// 循环数组
for (const value of arr) {
  console.log(value); // 1 2 3
}
// 循环普通对象
for (const value of obj) {
  console.log(value); // 报错: TypeError: obj is not iterable
}

之所以出现出现这个现象, 其实也好理解: 那是因为数组 原型 上内置了默认的 Symbol.iterator 方法, 实现了 可迭代协议, 所以它是一个 可迭代对象; 但是呢, 普通对象是没有内置 Symbol.iterator 方法的, 所以它并 不是 一个 可迭代对象

const arr = [1, 2, 3]

const obj = {
  name: 'moyuanjiun'
}

if (arr[Symbol.iterator]) {
  console.log('数组是可迭代对象') // 数组是可迭代对象
}

if (!obj[Symbol.iterator]) {
  console.log('普通对象是不可迭代对象') // 普通对象是不可迭代对象
}

那么我们如果要 篡改 对象的默认 迭代行为 也很简单, 只需要修改默认的 Symbol.iterator 方法即可, 如下代码: 通过修改 Symbol.iterator 方法, 篡改了数组 arr 的迭代行为, 每次迭代获取的值都会乘以 2

const arr = [1, 2, 3]

arr[Symbol.iterator] = function() {
  let index = 0
  return {
    next: () => {
      const ele = this[index++]
      if (ele) {
        return { value: 2 * ele, done: false }
      }
      return { value: undefined, done: true }
    }
  }
}

for (const value of arr) {
  console.log(value) // 2 4 6
}

同样的 普通对象 是无法被 迭代 的, 但如果我们设置了 Symbol.iterator 属性就能够让普通对象允许被迭代, 如下代码: 我们为 obj 定义了 Symbol.iterator 方法, 该方法返回一个 迭代器

const obj = {
  [Symbol.iterator]() {
    let index = 0

    return {
      next: () => {
        if (index < 3) {
          return { value: index++, done: false }
        }
        return { value: undefined, done: true }
      }
    }
  }
}
for (const value of obj) {
  console.log(value) // 0 1 2
}

那么问题来了 JS 中常见的数据中, 有哪些数据是默认允许被迭代的呢? 主要有下面几种:

  • Array
  • string
  • Set
  • Map

2.4 可迭代迭代器

已知 迭代器 本质上就是个对象, 我们其实很容易使它变为 可迭代对象, 只需实现 Symbol.iterator 方法, 并返回它的 this 即可, 这时该对象我们可称之为 可迭代迭代器, 如下代码: obj可迭代迭代器 使用 for...of 就能够直接循环输出

const obj = {
  index: 0,
  next(){
    if (this.index < 3) {
      return { value: this.index++, done: false }
    }
    return { value: undefined, done: true }
  },
  [Symbol.iterator](){
    return this
  }
}

for (const value of obj) {
  console.log(value) // 0 1 2
}

其实在不实现 可迭代协议 的情况下, 仅实现 迭代器协议 的作用很小, 大部分 迭代器 都实现了 可迭代协议, 包括后面要讨论的 生成器, 它返回的就是 可迭代迭代器

三、异步迭代器

异步迭代器迭代器 约定基本一致, 唯一不同的是 异步迭代器nextreturnthrow 等方法返回的 IteratorResult 接口对象中, value 值是一个 Promise 实例, 如下代码所示: obj 是一个 异步迭代器, next() 方法返回的对象中 value 是一个 Promise 实例

const obj = {
  index: 0,
  next(){
    if (this.index < 3) {
      return {
        done: false,
        // value 为 Promise 实例
        value: new Promise(resolve => {
          setTimeout(() => resolve(this.index), 1000 * this.index++)
        }),
      }      
    }

    return {  done: true, value: Promise.resolve(undefined) }
  }
}

上面例子中, 每次执行 next() 进行迭代取值, 拿到的 value 值是个 Promise 实例, 下面是一个调用 异步迭代器 的简单例子

const obj = {
  index: 0,
  next(){
    if (this.index < 3) {
      return {
        done: false,
        value: new Promise(resolve => {
          setTimeout(() => resolve(this.index), 1000 * this.index++)
        }),
      }      
    }

    return {  done: true, value: Promise.resolve(undefined) }
  }
}

const run = async () => {
  const res1 = await obj.next().value // 0 秒后输出: 1
  const res2 = await obj.next().value // 1 秒后输出: 2
  const res3 = await obj.next().value // 2 秒后输出: 3
  const res4 = await obj.next().value // 3 秒后输出: undefined
}

run()

四、异步可迭代协议

异步可迭代协议可迭代协议 约定基本一致, 只是它是通过 Symbol.asyncIterator 方法来设置 对象的迭代行为, 并且该方法返回的是一个 异步迭代器, 如下代码: obj 定义了 Symbol.asyncIterator 方法, 实现了 异步可迭代协议, 我们可以称之为 异步可迭代对象

const obj = {
  [Symbol.asyncIterator](){
    let index = 0;
    return {
      next(){
        if (index < 3) {
          return {
            done: false,
            value: new Promise(resolve => {
              setTimeout(() => resolve(index), 1000 * index++)
            }),
          }      
        }
        return {  done: true, value: Promise.resolve(undefined) }
      }
    }
  }
}

对于 可迭代对象, 我们可使用 for...of 进行迭代, 那么对于 异步可迭代对象, 如果使用 for...of 进行迭代是会抛出错误, 因为我们并没有定义 Symbol.iterator, 它是一个 不可迭代对象, 如下代码: 我们尝试使用 for...of 来迭代 异步可迭代对象, 最后会报 obj is not iterable

const obj = {
  [Symbol.asyncIterator](){
    let index = 0;

    return {
      next(){
        if (index < 3) {
          return {
            done: false,
            value: new Promise(resolve => {
              setTimeout(() => resolve(index), 1000 * index++)
            }),
          }      
        }
        return {  done: true, value: Promise.resolve(undefined) }
      }
    }
  }
}

for (const value of obj) { // obj is not iterable
  console.log(value)
}

那么我们又该怎么循环 异步可迭代对象 呢?这里可以使用 for await ... of 来循环 异步可迭代对象, 如下代码: 演示了如何使用 for await ... of 来循环 异步可迭代对象

const obj = {
  [Symbol.asyncIterator](){
    let index = 0;

    return {
      next(){
        if (index < 3) {
          return {
            done: false,
            value: new Promise(resolve => {
              setTimeout(() => resolve(index), 1000 * index++)
            }),
          }      
        }
        return {  done: true, value: Promise.resolve(undefined) }
      }
    }
  }
}

for await (const promise of obj) {
  // 这里这里拿到的值是 Promise 实例
  const value =  await promise
  console.log(value) // 立即打印出: 1, 1 秒后打印 2, 2 秒后打印出 3
}

这里卖个关子: 上面例子中, 为什么打印出的是 1 2 3, 而不是 0 1 2, 欢迎在评论区进行讨论, 也可以进行简单改造, 让它打印出 0 1 2

其实循环 异步可迭代对象 和循环 可迭代对象 逻辑基本一致:

  1. 先调用对象的 Symbol.asyncIterator 方法, 生成一个 异步迭代器

  2. 循环调用 异步迭代器next() 方法, 方法会返回具有 valuedone 两个属性的对象, value 是一个 Promise 实例, done 则表示是否遍历结束

  3. 每次判断 done 是否为 true, 如果是则循环会自动结束

下面是演示代码: 方法 forAwaitOf 模拟了循环 异步可迭代对象 的流程, 并且打印出了每个 Promise 实例

const obj = {
  [Symbol.asyncIterator](){
    let index = 0;

    return {
      next(){
        if (index < 3) {
          return {
            done: false,
            value: new Promise(resolve => {
              setTimeout(() => resolve(index), 1000 * index++)
            }),
          }      
        }
        return { done: true, value: Promise.resolve(undefined) }
      }
    }
  }
}


const forAwaitOf = (obj) => {
  const asyncIterator = obj[Symbol.asyncIterator]()
  let done = false

  while (!done) {
    const current = asyncIterator.next()
    done = current.done
    if (!done){
      console.log('forAwaitOf', current.value) 
    }
  }
}

forAwaitOf(obj)

五、生成器

生成器 是一种特殊的 JS 函数, 它使用 function* 关键字来进行定义, 该函数会返回一个 Generator 对象, 该对象是符合 迭代器协议 的, 所以它本质上就是个 迭代器

当我们执行 生成器 函数将会返回一个 迭代器, 它并不会直接执行函数体里面的代码, 如下代码: 函数里的代码并没有执行, 函数返回一个 Generator 对象, 也是一个 迭代器

function* generator (){
  console.log('这段代码不会被执行')
}

const res = generator()

console.log(res) // Object [Generator] {}

生成器 中可使用 yield 关键字来暂停代码的执行, yield 执行逻辑如下:

  • 调用 生成器 会返回一个 迭代器
  • 当我们调用 迭代器next() 方法将执行函数里面的代码, 执行过程中如果遇到 yield 关键词, 函数的执行会被暂停, yield 关键词 后面的值 将被作为 next() 方法的 value 值被返回
  • 下一次再次执行 next() 方法时, 代码将从暂停的地方继续执行, 直到再次遇到 yield 关键词, 函数的执行又会被暂停, yield 关键词 后面的值 作为 next() 方法的 value 值被返回
  • 如此往复, 直到函数内代码全部执行完毕, next() 方法中 done 将被设置为 ture, 整个 迭代结束

有如下代码: 创建了一个 生成器 generator, 并执行 生成器 返回了一个 迭代对象 gen, 然后依次调用 迭代对象next() 方法

function* generator() {
  // 第一次 next 执行下列代码
  let a = 1;
  let b = 1;
  yield a + b

  // 第二次 next 执行下列代码
  a = 2
  b = 3
  yield a + b

  // 第三次 next 执行下列代码
  a = 9
  b = 3
  console.log('函数体代码执行结束:', a / b)
}

const gen = generator()
const res1 = gen.next() // { value: 2, done: false }
const res2 = gen.next() // { value: 5, done: false }
const res3 = gen.next() // { value: undefined, done: true }
const res4 = gen.next() // { value: undefined, done: true }, 因为循环已经结束, 不会执行任何代码

为每个关键节点添加了 debugger, 下图是 debug 的一个演示图:

klx.pro.S2FwdHVyZSAyMDIzLTAyLTI0IGF0IDEyLjU5LjE1LmdpZjE2NzcyMTQ4MDA3MDA=.gif

5.1 定义「可迭代对象」

上文中提到, 要想实现一个 可迭代对象, 只需要遵循 可迭代协议, 只需要定义出符合要求的 Symbol.iterator 方法即可, 该方法返回一个 迭代器

读到这里你是不是会发现, 可迭代对象Symbol.iterator 方法其实和 生成器 很像, 他们都是一个用于生产 迭代器 的一个方法, 生成器 实际上是一个语法糖, 它的作用其实就是为了让我们更方便、更快速的创建一个 迭代器

下面代码: 我们使用 生成器 来为对象定义 Symbol.iterator 方法, 来实现一个 可迭代对象, 从代码上也可以发现使用 生成器 可以很容易创建一个 可迭代对象, 而且理解起来也很简单(每个 yield 后面的值, 就是每次迭代输出的 value

const obj = {
  *[Symbol.iterator](){
    yield 1
    yield 2
    yield 3
  }
}

for (const value of obj) {
  console.log(value) // 1 2 3
}

5.2「生成器」返回「可迭代迭代器」

生成器 返回一个 迭代器, 打印该 迭代器 会发现该对象内部实现了 Symbol.iterator 方法, 也就是说 生成器 返回的 迭代器 是一个 可迭代迭代器, 如下代码: 直接使用 for...of 循环 生成器 返回的 迭代器, 会发现能够正常循环的, 因为它是一个 可迭代迭代器

function* generator(length) {
  let index = 0
  while(index < length) {
    yield index
    index ++ 
  }
}

// 直接循环生成器产物
for (let value of generator(3)) {
  console.log(value); // 0 1 2
}

5.3 生成器传参

既然 生成器 是个函数, 那我们自然就能够为它设置参数, 如下代码: 通过变量 length 设置了 生成器 最终返回的 迭代器 可迭代次数

function* generator(length = 3) {
  let index = 0

  while (index < length) {
    yield index * 2
    index++
  }
}

const gen = generator(5)

for (const value of gen) {
  console.log(value) // 0 2 4 6 8
}

5.4 next() 传参

在这里试想下, 如果我们为 next() 传参那么这个参数将会被怎么调用的呢?

看下面代码, 从执行结果是否能发现到它们的规律呢?用一句话来讲就是: 当前 next() 方法的的参数, 将会作为上一句 yield 关键词的结果赋值给 左侧 的变量, 如下代码: 生成器 中每个 yield 关键词都会返回上一次 yield 的结果

function* generator(data) {
  const first = yield data

  const second = yield first

  const third = yield second

  yield third
}

const gen = generator(10)

console.log('1', gen.next()); // 1 { value: 10, done: false }
console.log('2', gen.next(20)); // 2 { value: 20, done: false }
console.log('3', gen.next("moyuanjun")); // 3 { value: 'moyuanjun', done: false }
console.log('4', gen.next({ age: 18 })); //4 { value: { age: 18 }, done: false }
console.log('5', gen.next(0)); // 5 { value: undefined, done: true }

其实你也可以这么理解, 赋值语句实际上是先执行右侧的表达式, 然后才进行赋值的, 在执行赋值语句右侧表达式时遇到了 yield 代码时就停止执行, 这时还未完成赋值操作, 当再次执行 next() 才会开始赋值, 这时赋值的值来自于 next() 的参数

5.5 yield*

yield* 后面可以跟着一个 可迭代对象, yield* 会优先迭代完后面的 可迭代对象, 再继续向下迭代, 如下代码所示: 遇到 yield* 关键词会先迭代完数组, 再继续往下

function* anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function* generator(i){
  yield i;
  yield* [1, 2, 3]; // yield* 后面跟着一个「可迭代对象」
  yield i + 10;
}

const gen = generator(0)

gen.next() // { value: 0, done: false }
gen.next() // { value: 1, done: false }
gen.next() // { value: 2, done: false }
gen.next() // { value: 3, done: false }
gen.next() // { value: 10, done: false }
gen.next() // { value: undefined, done: true }

已知 生成器 返回的是 可迭代迭代器, 那么我们这里就能够使用 yield* 关键词进行迭代, 相当于 迭代器 内部调用其他 迭代器, 实现 迭代器 之间的嵌套使用

function* anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function* generator(i){
  yield i;
  yield* anotherGenerator(i); // yield* 后面跟「生成器」返回的 「可迭代迭代器」
  yield i + 10;
}

const gen = generator(0)

for (const value of gen) {
  console.log(value) // 0 1 2 3 10
}

5.6 return()、throw()

根据 迭代器协议, 迭代器 具有可选属性 return() throw(), 这两个方法都是用于提前结束迭代, 可想而知 生成器 创建的 迭代器 也必然具有 return() throw() 这两个方法

如下代码: gen1gen2 是同一个迭代器生成的, gen1 进行正常迭代, gen2 迭代一次后, 调用了 return() 提前结束了迭代, 同时方法返回的 value 值等于传入的参数、done 值等于 true

function* generator(){
  yield 1
  yield 2
  yield 3
}

const gen1 = generator()

gen1.next() // { value: 1, done: false }
gen1.next() // { value: 2, done: false }
gen1.next() // { value: 3, done: false }
gen1.next() // { value: undefined, done: true }

const gen2 = generator()

gen2.next() // { value: 1, done: false }
gen2.return('提前结束') // { value: '提前结束', done: true }
gen2.next() // { value: undefined, done: true }

如下代码: gen1gen2 是同一个迭代器生成的, gen1 进行正常迭代, gen2 迭代一次后, 调用了 throw() 主动抛出一个错误并提前结束了迭代, 再次调用 next() 方法, 将会返回 { value: undefined, done: true }

function* generator(){
  yield 1
  yield 2
  yield 3
}

const gen1 = generator()

gen1.next() // { value: 1, done: false }
gen1.next() // { value: 2, done: false }
gen1.next() // { value: 3, done: false }
gen1.next() // { value: undefined, done: true }

const gen2 = generator()

gen2.next() //  { value: 1, done: false }
try {
  gen2.throw(new Error('抛出错误')) // 调用者捕获到错误, 通过 throw 抛出一个错误, 并结束迭代
} catch {}

gen2.next() // { value: undefined, done: true }

补充: 需要注意的是当我们调用 return()throw() 方法返回的 done 等于 true, 表明迭代已经结束, 所以这两个方法返回的 value 值是无效的, 如下代码: for...of 中提前结束了循环, return() 方法返回的值没有任何作用, 因为迭代已经结束

function* generator(){
  yield 1
  yield 2
  yield 3
}

const gen = generator()

for (const value of gen) {
  console.log(value) // 1

  if (value === 1) {
    gen.return(10)
  }
}

5.7 显式返回

生成器 中如果使用 return 提前结束函数的执行, 又是怎么的情况呢?

如下代码: 生成器 generatorreturn10, 迭代过程中遇到 return 语句会结束循环, next() 返回的 value 值等于 return 出来的值, done 等于 true

function* generator(){
  yield 1
  yield 2
  return 10
  yield 3
}

const gen = generator()

console.log(gen.next()) // { value: 1, done: false }
console.log(gen.next()) // { value: 2, done: false }
console.log(gen.next()) // { value: 10, done: true }
console.log(gen.next()) // { value: undefined, done: true }

同调用 return() 一样, 遇到 return 时, next() 返回的 donetrue, 表明迭代已经结束, 这时的 value 值是无效的, 如下代码所示: 使用 生成器obj 定义了 Symbol.iterator 方法, 生成器中 retun10, 用 for...of 进行循环输出, 会发现只输出了 1 2, 因为迭代过程中遇到 return 迭代已结束

function* generator(){
  yield 1
  yield 2
  return 10
  yield 3
}

for (const value of generator()) {
  console.log(value) // 1 2
}

5.8 生成「异步迭代器」

之前我们都没讨论过 yield 关键词后面允许跟哪些内容, 实际上它后面允许跟 任何类型数据任何表达式, yield 关键词后面如果是 表达式, 关键字的值将是 表达式 的返回值

如此, 根据 异步迭代器协议yield 关键词后面跟一个 Promise 实例, 那么该 生成器 将返回一个 异步迭代器, 如下代码: 通过 生成器异步可迭代对象 obj 定义了 Symbol.asyncIterator 方法, 生成器中 yield 跟着 Promise 实例, 最后使用 for await...of异步可迭代对象 进行循环

const obj = {
  *[Symbol.asyncIterator](){
    let i = 0
    while(i < 3) {
      yield new Promise((resole) => {
        setTimeout(() => resole(i), i * 1000)
      })
      i++
    }
  }
}

for await (const value of obj) {
  const res = await value
  console.log(res) // 立即打印 0, 1 秒后打印 1, 2 秒后打印 2
}

实际上 生成器 中不管 yield 关键词后面是不是 Promise 返回的 迭代器 都同时实现了 Symbol.iteratorSymbol.asyncIterator 方法, 所以它既是 可迭代迭代器 又是 异步可迭代迭代器, 也能够同时被 for...offor await...of 迭代, 如下代码:

function* generator() {
  yield 1
  yield 2
  yield 3
}

const gen = generator(10)

for (const value of gen) {
  console.log(value) // 1 2 3
}

for await (const value of gen) {
  console.log(await value()) // 1 2 3
}

六、生成器妙用

6.1 惰性求值

生成器可以使用 yield 暂停函数执行并返回一个值, 下次调用时会从上次暂停的地方继续执行; 因此, 可以在需要时造计算、生成值, 而不是一次性生成所有值, 如此可以大大减少内存消耗和提高性能, 尤其是对于无限序列的情况, 例如斐波那契数列, 如下代码: 是一个生成器, 用于生成前 n 个斐波那契数:

function* fibonacci(n) {
  let a = 0, b = 1;
  for (let i = 0; i < n; i++) {
    yield a;
    [a, b] = [b, a + b];
  }
}

for (let n of fibonacci(10)) {
  console.log(n);
}

6.2 异步编程

生成器可以与 Promise 结合使用来编写异步代码, 从而使异步代码更易于编写和阅读, 使用生成器编写的异步代码可以更像同步代码, 并且可以使用 try/catch 语句来捕获错误:

function* asyncOperation(base) {
  const step1 = yield new Promise(resolve => setTimeout(() => {
    console.log('执行第一个步骤=>')
    resolve(base + 1)
  }, 1000));

  const step2 = yield new Promise(resolve => setTimeout(() => {
    console.log('执行第二个步骤=>')
    resolve(step1 * 2)
  }, 1000));

  yield new Promise(resolve => setTimeout(() => {
    console.log('执行第三个步骤=>')
    resolve(step2 * 10)
  }, 1000));
}

const gen = asyncOperation(10);

try {
  // 1. 执行第一个步骤
  gen.next().value.then(step1 => {
    console.log(step1)

    // 如果 step1 为空, 结束迭代
    if (!step1) {
      gen.return()
      return
    }

    // 2. 执行第二个步骤
    gen.next(step1).value.then(step2 => {
      console.log(step2)
  
      // 3. 执行第三个步骤
      gen.next(step1).value.then(res => {
        console.log(res)
      })
    });
  })
} catch {
  // 捕获到错误, 结束迭代
  gen.throw(new Error('报错了'))
}

6.3 简化「迭代器」的实现

生成器 可以大大简化 迭代器 的实现, 因为 生成器 提供了内置的 迭代器 实现, 在 生成器 中我们只需要通过 yield 关键词定义每次迭代要生成的值即可:

const obj = {
  *[Symbol.iterator](){
    yield 1
    yield 2
    yield 3
  }
}

for (const value of obj) {
  console.log(value) // 1 2 3
}

6.4 用于数据处理

使用生成器可以实现数据处理, 例如将大型数据集分成小块进行处理, 或者从迭代器中获取一部分数据进行处理, 而不需要将整个数据集都加载到内存中, 下面是一个示例, 用于从一个大型数组中获取特定数量的元素并进行处理:

function* processArray(array, chunkSize) {
  let i = 0;
  while (i < array.length) {
    yield array.slice(i, i + chunkSize);
    i += chunkSize;
  }
}

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let chunk of processArray(array, 3)) {
  console.log(`Processing chunk: ${chunk}`); // 处理每个块
}

6.5 实现协程

协程是一种特殊的函数, 它可以在执行过程中暂停并保存当前状态, 等待下一次执行时恢复状态, 并在不同的协程之间进行切换

JS 中, 可以使用 生成器函数yield 关键字实现协程, 具体来说,协程函数 可以使用 yield 关键字暂停执行, 并返回一个值给调用方。调用方可以通过 next() 方法向 协程函数 发送数据, 并在 协程函数 中使用 yield 关键字接收数据, 协程函数 可以保存当前的状态, 并在下一次执行时恢复状态, 从而实现在不同的协程之间进行切换, 如下代码: 展示了使用 yield 关键字实现协程的过程

function* coroutine() {
  let value = yield;
  console.log(`Received value: ${value}`);

  value = yield value * 2;
  console.log(`Received value: ${value}`);

  value = yield value * 3;
  console.log(`Received value: ${value}`);
}

let gen = coroutine();

gen.next();
gen.next(1); // Received value: 1
gen.next(2); // Received value: 2, return value: 4
gen.next(3); // Received value: 3, return value: 9

七、总结

  • 迭代器 是一个实现了 迭代器协议 的对象, 该对象主要定义了一个 next() 方法, 方法返回一个 valuedone, value 表示当前迭代的值, done 则表示当前迭代是否结束

  • 异步迭代器 是一个实现了 异步迭代器协议 的对象, 它和 迭代器 的约定基本一致, 只是 next() 方法返回的 value 值规定是一个 Promise 实例

  • 可迭代对象 是一个实现了, 可迭代协议 的一个对象, 该对象定义了 Symbol.iterator 方法, 该方法返回一个 迭代器, 该对象能够被 for...of 进行迭代

  • 异步可迭代对象 是一个实现了, 异步可迭代协议 的一个对象, 该对象定义了 Symbol.asyncIterator 方法, 该方法返回一个 迭代器, 该对象能够被 for await...of 进行迭代

  • 生成器 是一个函数, 该函数返回一个 迭代器, 生成器 可以方便我们快速创建一个 迭代器, 提供一些非常强大和灵活的功能

  • 生成器 返回的 迭代器, 实现了 Symbol.iteratorSymbol.asyncIterator 方法, 所以它也是 可迭代迭代器异步可迭代迭代器, 能够被 for...offor await...of 循环 x

八、参考

Group 3143