Js 中迭代器、生成器详解!

序言

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

一、迭代器协议

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

1.1 协议的基本约定

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

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

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

js 复制代码
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 接口中, done 和 value 都不是严格要求的, 如果返回没有任何属性的对象, 实际上等价于 { done: false, value: undefined }

js 复制代码
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() 方法, 方法返回一个具有 value 和 done 属性的对象

1.2 return 方法

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

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

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

js 复制代码
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 接口对象, 对象中 done 为 true 表示迭代结束

js 复制代码
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 就被称为 可迭代对象

js 复制代码
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() 方法, 方法会返回具有 value 和 done 两个属性的对象, value 表示当前迭代的值, done 则表示是否遍历结束。

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

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

js 复制代码
// 可迭代对象
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 来循环迭代对象, 会发现和上面打印结果是一样样滴

js 复制代码
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

js 复制代码
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 方法的, 所以它并 不是 一个 可迭代对象

js 复制代码
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

js 复制代码
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 方法, 该方法返回一个 迭代器

js 复制代码
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 就能够直接循环输出

js 复制代码
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
}

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

三、异步迭代器

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

js 复制代码
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 实例, 下面是一个调用 异步迭代器 的简单例子

js 复制代码
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 方法, 实现了 异步可迭代协议, 我们可以称之为 异步可迭代对象

js 复制代码
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

js 复制代码
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 来循环 异步可迭代对象

js 复制代码
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() 方法, 方法会返回具有 value 和 done 两个属性的对象, value 是一个 Promise 实例, done 则表示是否遍历结束

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

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

js 复制代码
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 对象, 也是一个 迭代器

js 复制代码
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() 方法

js 复制代码
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 的一个演示图:

5.1 定义「可迭代对象」

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

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

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

js 复制代码
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 循环 生成器 返回的 迭代器, 会发现能够正常循环的, 因为它是一个 可迭代迭代器

js 复制代码
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 设置了 生成器 最终返回的 迭代器 可迭代次数

js 复制代码
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 的结果

js 复制代码
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* 关键词会先迭代完数组, 再继续往下

js 复制代码
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* 关键词进行迭代, 相当于 迭代器 内部调用其他 迭代器, 实现 迭代器 之间的嵌套使用

js 复制代码
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() 这两个方法

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

js 复制代码
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 }

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

js 复制代码
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() 方法返回的值没有任何作用, 因为迭代已经结束

js 复制代码
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 提前结束函数的执行, 又是怎么的情况呢?

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

js 复制代码
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() 返回的 done 为 true, 表明迭代已经结束, 这时的 value 值是无效的, 如下代码所示: 使用 生成器 为 obj 定义了 Symbol.iterator 方法, 生成器中 retun 出 10, 用 for...of 进行循环输出, 会发现只输出了 1 2, 因为迭代过程中遇到 return 迭代已结束

js 复制代码
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 对 异步可迭代对象 进行循环

js 复制代码
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.iterator 和 Symbol.asyncIterator 方法, 所以它既是 可迭代迭代器 又是 异步可迭代迭代器, 也能够同时被 for...of 和 for await...of 迭代, 如下代码:

js 复制代码
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 个斐波那契数:

js 复制代码
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 语句来捕获错误:

js 复制代码
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 关键词定义每次迭代要生成的值即可:

js 复制代码
const obj = {
  *[Symbol.iterator](){
    yield 1
    yield 2
    yield 3
  }
}

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

6.4 用于数据处理

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

js 复制代码
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 关键字实现协程的过程

js 复制代码
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() 方法, 方法返回一个 value 和 done, value 表示当前迭代的值, done 则表示当前迭代是否结束

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

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

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

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

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

    x

八、参考


相关推荐
白帽攻防录11 小时前
SRC 挖洞:Roundcube 预认证 SQL 注入深度复盘,CVE-2026-48842 preg_replace 转义绕过怎么打穿邮件系统
网络·数据库·sql·网络安全·sql注入
左手の明天11 小时前
SLIP 协议封装与解析详解:原理、C 代码与实战案例
linux·c语言·开发语言·c++
扶风ff11 小时前
新品知识更新太快?用练题簿在线刷题,安排企业培训的小测与复盘
前端·学习·小程序
海上小飞龙11 小时前
【KMP算法-下篇】同一道题:Java 库函数 2600 微秒,手写 KMP 16 微秒
java·开发语言·算法
对空六课12 小时前
支持注意力分析的热力图工具有哪些?
前端·数据库·数据分析
朝朝辞暮i12 小时前
C++ 第 12 课:局部变量、作用域、变量生命周期
开发语言·c++·算法
狗凯之家源码网12 小时前
淘客口令落地页生成器 PHP 版深度评测
开发语言·php
画绛集美术12 小时前
本地化部署LLM用于美术史问答:一间美术教室的实践记录
开发语言·php
八解毒剂12 小时前
【计组】中央处理器CPU
java·开发语言·计算机组成原理
Csvn13 小时前
Vue3 响应式与编译:依赖收集如何升级为节点级靶向更新
前端