第 22 章

生成器与迭代协议:Symbol.iterator 的完整实现

生成器函数调用时不执行函数体,而是返回一个生成器对象;每次调用 next() 才真正向前推进——这不是语法糖,而是将函数执行权在调用者和函数体之间来回传递的协作式多任务机制。

🔹 Level 1 · 你需要知道的

生成器的基本用法

function* 定义生成器函数,yield 是暂停点。

function* counter(start = 0) {
  while (true) {
    yield start++   // 暂停,向外返回 start 的当前值,然后递增
  }
}

const gen = counter(10)
console.log(gen.next())  // { value: 10, done: false }
console.log(gen.next())  // { value: 11, done: false }
console.log(gen.next())  // { value: 12, done: false }
// 可以无限调用,不会求值到最后(惰性)

调用 counter(10) 时函数体不执行,只是创建了一个生成器对象。第一次 gen.next() 才开始执行,执行到第一个 yield 暂停。

for...of 消费可迭代对象

for...of 自动调用迭代器协议。任何实现了 [Symbol.iterator]() 方法的对象都可以用 for...of 遍历:

// 内置可迭代:Array、String、Map、Set、arguments、NodeList 等
for (const char of 'hello') {
  console.log(char)   // 'h', 'e', 'l', 'l', 'o'
}

// 生成器对象本身就是可迭代的
function* range(start, end) {
  for (let i = start; i < end; i++) {
    yield i
  }
}

for (const n of range(1, 5)) {
  console.log(n)  // 1, 2, 3, 4
}

// 展开运算符和解构也使用迭代协议
const arr = [...range(1, 5)]  // [1, 2, 3, 4]
const [first, second] = range(1, 5)  // first=1, second=2

向生成器传值

next(value) 的参数成为对应 yield 表达式的值,实现双向通信:

function* dialog() {
  const name = yield '你叫什么名字?'   // 暂停,等待下次 next() 传入名字
  const age = yield `${name},你多大了?`
  yield `${name},${age}岁,很高兴认识你!`
}

const gen = dialog()
console.log(gen.next().value)          // '你叫什么名字?'
console.log(gen.next('Alice').value)   // 'Alice,你多大了?'
console.log(gen.next(28).value)        // 'Alice,28岁,很高兴认识你!'

无限序列的惰性求值

生成器是实现惰性求值的天然工具。斐波那契数列生成器:

function* fibonacci() {
  let [a, b] = [0, 1]
  while (true) {
    yield a
    ;[a, b] = [b, a + b]
  }
}

// 取前10个斐波那契数
function take(n, iterable) {
  const result = []
  for (const value of iterable) {
    result.push(value)
    if (result.length >= n) break
  }
  return result
}

console.log(take(10, fibonacci()))
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

🔸 Level 2 · 它是怎么运行的

迭代协议:两个接口

ECMAScript 的迭代协议由两个独立的接口组成:

迭代协议的两个接口:

  Iterable(可迭代对象接口)
  ┌────────────────────────────────────────┐
  │ [Symbol.iterator](): Iterator          │
  │   调用后返回一个 Iterator 对象          │
  └────────────────────────────────────────┘

  Iterator(迭代器接口)
  ┌────────────────────────────────────────┐
  │ next(): IteratorResult                 │
  │   必选,推进迭代器                      │
  │                                        │
  │ return(value?): IteratorResult         │
  │   可选,提前终止(用于清理资源)         │
  │                                        │
  │ throw(exception?): IteratorResult      │
  │   可选,向迭代器注入异常                │
  └────────────────────────────────────────┘

  IteratorResult
  ┌────────────────────────────────────────┐
  │ value: any   当前迭代值                │
  │ done: boolean  true 表示迭代完成        │
  └────────────────────────────────────────┘

生成器对象同时实现两个接口——它既是 Iterable(gen[Symbol.iterator]() === gen),也是 Iterator(有 next/return/throw 方法)。

生成器内部的4状态机

生成器对象的 [[GeneratorState]] 内部槽存储当前状态,状态转换如下:

生成器状态机:

             创建时
              │
              ▼
    ┌──────────────────┐
    │  suspendedStart  │  ← 刚被创建,函数体还未开始执行
    └──────────────────┘
              │
              │ 第一次 next()
              ▼
    ┌──────────────────┐   yield expr
    │    executing     │ ──────────────► ┌──────────────────┐
    │  (函数体运行中)  │                 │  suspendedYield  │
    └──────────────────┘ ◄────────────── └──────────────────┘
              │             下次 next()
              │
              │ return / throw / 函数体结束
              ▼
    ┌──────────────────┐
    │    completed     │  ← 终止状态,后续 next() 返回 {value:undefined, done:true}
    └──────────────────┘

状态转换规则:
  suspendedStart  → executing       :第一次 next() 调用
  executing       → suspendedYield  :执行到 yield 表达式
  suspendedYield  → executing       :下次 next()/return()/throw() 调用
  executing       → completed       :return 语句 / 函数体执行完毕 / 未捕获异常
  任意状态调用 return() → completed  :强制终止
function* gen() {
  console.log('A')
  yield 1
  console.log('B')
  yield 2
  console.log('C')
}

const g = gen()
// 状态:suspendedStart,没有任何输出

g.next()
// 输出 'A',在 yield 1 处暂停
// 返回 { value: 1, done: false }
// 状态:suspendedYield

g.next()
// 输出 'B',在 yield 2 处暂停
// 返回 { value: 2, done: false }
// 状态:suspendedYield

g.next()
// 输出 'C',函数体执行完毕
// 返回 { value: undefined, done: true }
// 状态:completed

for...of 的内部执行步骤

for (const x of expr) 在规范中展开为:

for...of 的执行过程:

1. 对 expr 求值,得到对象 obj
2. 调用 GetIterator(obj, sync):
   a. 调用 obj[Symbol.iterator]() 得到 iterator
   b. 验证 iterator 是对象(否则 TypeError)
3. 循环开始:
   a. 调用 iterator.next(),得到 iterResult
   b. 检查 iterResult.done:
      - done === true:退出循环
      - done === false:将 iterResult.value 赋给循环变量,执行循环体
4. 若循环体因 break 提前退出:
   a. 若 iterator.return 存在,调用 iterator.return()
      (用于释放生成器持有的资源,触发 finally 块)
5. 若循环体抛出异常:
   a. 若 iterator.return 存在,调用 iterator.return()
   b. 重新抛出异常
function* withCleanup() {
  try {
    yield 1
    yield 2
    yield 3
  } finally {
    console.log('cleanup!')  // break 时会触发
  }
}

for (const n of withCleanup()) {
  console.log(n)
  if (n === 2) break  // 触发 iterator.return(),进而触发 finally
}
// 输出:1  2  cleanup!

yield* 的工作机制

yield* 将迭代委托给另一个可迭代对象,并且会转发 return()/throw() 调用:

yield* 的执行过程:

function* outer() {         │  调用方
  const result = yield* inner()  │
  console.log(result)       │
}                           │

yield* 做的事情:
1. 调用 inner()[Symbol.iterator]() 得到 innerIterator
2. 循环调用 innerIterator.next(value)
   每次把结果 {value, done:false} 转发给外层调用者
3. 当 innerIterator.done === true 时:
   innerIterator 的最终 value 作为 yield* 表达式的值(result)
4. 外层 next() 传入的 value 被转发给 innerIterator.next(value)
5. 外层 throw(e) 被转发给 innerIterator.throw(e)
6. 外层 return(v) 被转发给 innerIterator.return(v)
function* inner() {
  yield 'a'
  yield 'b'
  return 'inner done'  // 这是 yield* 的返回值
}

function* outer() {
  const result = yield* inner()
  console.log('inner returned:', result)  // 'inner done'
  yield 'c'
}

console.log([...outer()])  // ['a', 'b', 'c']
// 并且控制台输出:inner returned: inner done

向生成器注入异常

generator.throw(error) 在当前暂停的 yield 处抛出异常,生成器内部可以用 try/catch 捕获:

throw() 的执行流:

gen.throw(new Error('boom'))
  │
  ▼
生成器恢复执行,但当前 yield 表达式抛出 Error('boom')
  │
  ├── 有 try/catch 包裹当前 yield:
  │     catch 块捕获异常,继续执行
  │     下一个 yield 的值返回给 throw() 的调用者
  │
  └── 没有 try/catch:
        异常冒泡到生成器外部
        生成器变为 completed 状态
function* safeDivide() {
  let result
  try {
    const a = yield '输入被除数'
    const b = yield '输入除数'
    result = a / b
  } catch (e) {
    console.log('捕获到错误:', e.message)
    result = 0
  }
  return result
}

const gen = safeDivide()
gen.next()          // { value: '输入被除数', done: false }
gen.next(10)        // { value: '输入除数', done: false }
gen.throw(new Error('除数不能为零'))
// 输出:捕获到错误: 除数不能为零
// 返回:{ value: 0, done: true }

自定义可迭代对象

让普通对象支持 for...of,只需实现 [Symbol.iterator]() 方法:

const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from
    const last = this.to
    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false }
        }
        return { value: undefined, done: true }
      }
    }
  }
}

console.log([...range])  // [1, 2, 3, 4, 5]
for (const n of range) {
  console.log(n)  // 1 2 3 4 5
}

🔺 Level 3 · 规范怎么定义的

27.1 Iteration 协议的规范表述

规范 27.1.1 定义 Iterable Interface:对象必须支持 @@iterator 方法(即 [Symbol.iterator]),该方法无参调用时返回一个满足 Iterator Interface 的对象。

规范 27.1.2 定义 Iterator Interface

规范 27.1.3 定义 AsyncIterator Interface:与同步版本相同,但 next()/return()/throw() 返回 Promise(用于 for await...of)。

GeneratorObject 的内部槽

生成器对象是 Generator Exotic Object,内部槽如下:

GeneratorObject 内部槽:

┌──────────────────────┬──────────────────────────────────────────┐
│ [[GeneratorState]]   │ suspendedStart / executing /             │
│                      │ suspendedYield / completed               │
├──────────────────────┼──────────────────────────────────────────┤
│ [[GeneratorContext]] │ 生成器被挂起时保存的执行上下文              │
│                      │(包含变量绑定、当前 PC 等)                │
├──────────────────────┼──────────────────────────────────────────┤
│ [[GeneratorBrand]]   │ 生成器品牌(AsyncGeneratorObject 用来    │
│                      │ 区分普通 Generator)                      │
└──────────────────────┴──────────────────────────────────────────┘

[[GeneratorContext]] 是挂起时的完整执行上下文快照:它保存当前的变量环境、词法环境、代码执行位置(程序计数器指向哪条指令)。调用 next() 时,引擎将这个上下文推回执行上下文栈并从断点继续执行。

GeneratorResume 算法

规范 27.5.3.3 GeneratorResume(generator, value, generatorBrand):

GeneratorResume(generator, value, generatorBrand):

1. 令 state = generator.[[GeneratorState]]

2. 若 state === 'completed':
     返回 CreateIterResultObject(undefined, true)

3. 断言:state === 'suspendedStart' 或 'suspendedYield'

4. 令 genContext = generator.[[GeneratorContext]]

5. 令 methodContext = 运行中的执行上下文

6. 将 genContext 推入执行上下文栈,使其成为运行中的执行上下文

7. 将 generator.[[GeneratorState]] 设为 'executing'

8. 若 state === 'suspendedStart':
     直接开始执行函数体(第一次 next())
   否则(suspendedYield):
     将 value 作为 yield 表达式的结果返回给函数体

9. 执行函数体,直到下一个 yield 或函数体结束

10. 执行上下文栈恢复

11. 若遇到 yield expr:调用 GeneratorYield(value)
    若函数体结束(return value):
      generator.[[GeneratorState]] = 'completed'
      返回 CreateIterResultObject(returnValue, true)

GeneratorYield 算法

规范 27.5.3.7 GeneratorYield(iterNextObj):

GeneratorYield(iterNextObj):

1. 令 genContext = 运行中的执行上下文
   // genContext 是生成器的执行上下文

2. 令 generator = genContext 关联的生成器对象

3. 断言:generator.[[GeneratorState]] === 'executing'

4. 将 generator.[[GeneratorState]] 设为 'suspendedYield'

5. 从执行上下文栈弹出 genContext
   将 generator.[[GeneratorContext]] = genContext
   // 保存当前执行状态,包含 PC 位置和所有局部变量

6. 令 callerContext = 执行上下文栈顶(调用方的上下文)
   恢复 callerContext

7. 向调用方返回 iterNextObj(即 {value: yieldValue, done: false})

8. 注意:GeneratorYield 本身不会立即返回
   它在下一次 GeneratorResume 时才"返回"——
   yield 表达式的值是下次 next(value) 的参数

GetIterator 算法

规范 7.4.1 GetIterator(obj, hint, method):

GetIterator(obj, sync):

1. 若 method 未指定:
   - hint 是 'sync':method = GetMethod(obj, @@iterator)
   - hint 是 'async':method = GetMethod(obj, @@asyncIterator)
   
2. 调用 Call(method, obj),得到 iterator

3. 若 iterator 不是对象:抛出 TypeError

4. 令 nextMethod = GetMethod(iterator, 'next')

5. 返回 { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }
   // 规范称此为 IteratorRecord

💎 Level 4 · 边界与陷阱

陷阱 1:yield* 的返回值容易被忽略

yield* 会收集内部迭代器的最终返回值(done: true 时的 value),但只有通过 const result = yield* inner() 才能获取到;直接用 for...of 消费时,最终值(return 语句的值)会被丢弃

function* gen() {
  return 'final'  // done: true 时 value 是 'final'
}

// 通过 yield* 获取
function* outer() {
  const result = yield* gen()
  console.log(result)  // 'final'
}
[...outer()]

// 通过 for...of 消费时,return 的值被丢弃
for (const v of gen()) {
  console.log(v)  // 不会打印任何东西(gen() 没有 yield)
}
// 无输出,'final' 被丢弃

// 通过手动调用 next() 才能看到 return 值
const g = gen()
console.log(g.next())  // { value: 'final', done: true }

陷阱 2:generator.return()finally 的交互

调用 generator.return(value) 会让生成器立即进入 completed 状态,但如果生成器内部有 finally 块,finally 会执行;并且如果 finally 里有 yield,生成器会再次暂停:

function* gen() {
  try {
    yield 1
    yield 2
  } finally {
    console.log('finally 执行')
    yield 'from finally'  // finally 里的 yield!
    console.log('finally 结束')
  }
}

const g = gen()
console.log(g.next())         // { value: 1, done: false }
console.log(g.return('end'))
// 输出:finally 执行
// 返回:{ value: 'from finally', done: false }(因为 finally 里有 yield!)

console.log(g.next())
// 输出:finally 结束
// 返回:{ value: 'end', done: true }(finally 结束后才返回 'end')

这是规范明确定义的行为(27.5.3.5 GeneratorReturn):若 finally 块中执行了 yieldreturn() 的返回值会被暂存,等 finally 执行完毕后再返回。

陷阱 3:第一次 next() 传入的值被丢弃

第一次调用 next(value) 时,value 永远被丢弃。因为第一次 next() 是从函数开头开始执行的,没有任何 yield 表达式在等待接收值:

function* gen() {
  const x = yield 'first'   // 第二次 next(value) 的 value 会成为 x
  const y = yield 'second'  // 第三次 next(value) 的 value 会成为 y
  return x + y
}

const g = gen()
console.log(g.next(999))    // { value: 'first', done: false },999 被丢弃
console.log(g.next(10))     // { value: 'second', done: false },x = 10
console.log(g.next(20))     // { value: 30, done: true },y = 20,返回 x+y=30

规范原因:GeneratorResume 中,若状态是 suspendedStart(第一次),直接开始执行,value 参数没有被使用。

陷阱 4:迭代器和可迭代对象的混淆

for...of 和展开运算符期望的是 Iterable(有 [Symbol.iterator]() 的对象),而不是 Iterator(有 next() 的对象)。但生成器对象恰好同时是两者:

// 手写迭代器(只有 next,没有 Symbol.iterator)
const iterator = {
  count: 0,
  next() {
    return this.count < 3
      ? { value: this.count++, done: false }
      : { value: undefined, done: true }
  }
}

// for...of 失败!
for (const v of iterator) {  // TypeError: iterator is not iterable
  console.log(v)
}

// 修复:让迭代器也是可迭代的
const fixedIterator = {
  count: 0,
  next() {
    return this.count < 3
      ? { value: this.count++, done: false }
      : { value: undefined, done: true }
  },
  [Symbol.iterator]() { return this }  // 返回自身
}

for (const v of fixedIterator) {  // 现在可以了
  console.log(v)  // 0, 1, 2
}

// 生成器对象天然同时实现两个接口
function* g() { yield 1; yield 2 }
const gen = g()
console.log(gen[Symbol.iterator]() === gen)  // true,返回自身

陷阱 5:异步生成器与 for await...of 的资源泄漏

异步生成器配合 for await...of 使用时,提前 break 会调用 return() 关闭生成器,但如果没有 finally 块,持有的外部资源(如数据库连接、文件句柄)可能不会被释放:

async function* dbRows(connection) {
  try {
    let cursor = await connection.openCursor()
    while (cursor.hasMore()) {
      yield await cursor.fetchNext()
    }
  } finally {
    await connection.close()  // 必须有 finally,保证资源释放
  }
}

// 正确使用
for await (const row of dbRows(conn)) {
  process(row)
  if (shouldStop(row)) break  // break 触发 return(),finally 里关闭连接
}

如果去掉 finallybreak 退出后连接可能永远不会关闭。每个持有外部资源的生成器都应该用 try...finally 包裹。


本章小结

  1. 迭代协议由两个接口组成:Iterable(有 [Symbol.iterator]())和 Iterator(有 next()),生成器对象同时满足两者,gen[Symbol.iterator]() === gen
  2. 生成器有 4 个状态:suspendedStartexecutingsuspendedYieldcompleted,每次 yield 保存完整执行上下文,下次 next() 从断点恢复。
  3. next(value) 第一次调用时 value 被丢弃;第二次及之后的 value 成为前一个 yield 表达式的值,实现双向通信。
  4. generator.return(value) 强制终止生成器,但若有 finally 块则先执行 finallyfinally 中的 yield 会再次暂停生成器。
  5. 持有外部资源(数据库连接、文件句柄等)的生成器必须用 try...finally 保证资源释放,因为调用方可能在任何时刻 break 跳出迭代。
本章评分
4.5  / 5  (7 评分)

💬 留言讨论