第59课:迭代器与生成器——Symbol.iterator、for-of、yield、next()
迭代器(Iterator)和生成器(Generator)是 JavaScript 中处理可遍历数据结构和惰性计算的核心机制。ES6 引入了统一的迭代协议——任何实现了 Symbol.iterator 方法的对象都被视为可迭代对象,可以配合 for-of 循环、展开运算符、解构赋值等语法使用。生成器则是一种特殊的函数,可以暂停和恢复执行,每次产出(yield)一个值,是实现自定义迭代器和异步流程控制的强大工具。本节课将系统讲解迭代协议的定义、内置可迭代对象、生成器函数的语法与执行模型,以及两者的实战组合。
1. 迭代协议:可迭代对象与迭代器
迭代协议由两部分组成:
- 可迭代协议(Iterable Protocol):对象必须实现
Symbol.iterator方法,该方法返回一个迭代器。 - 迭代器协议(Iterator Protocol):迭代器必须实现
next()方法,该方法返回{ value, done }对象。done: true表示遍历结束。
1 | const myIterable = { |
通过 for-of 循环,引擎自动调用 Symbol.iterator 获取迭代器,然后不断调用 next() 直到 done === true。这一机制也是展开运算符和数组解构的基础。
2. 内置可迭代对象
JavaScript 提供了多个内置可迭代对象:
| 类型 | 示例 |
|---|---|
| 字符串 | for (const ch of 'hello') {} |
| 数组 | for (const item of [1,2,3]) {} |
| TypedArray | Uint8Array、Int32Array |
| Map | for (const [k, v] of map) {} |
| Set | for (const val of set) {} |
arguments |
for (const arg of arguments) {}(普通函数) |
| NodeList | document.querySelectorAll('div') |
1 | // Map 迭代 |
3. for-of 与 for-in 的区别
for-in 遍历的是对象的可枚举属性键(包括原型链上的属性),适用于普通对象,但不应该用于数组(因为它会遍历属性名,且顺序不可靠)。for-of 遍历的是可迭代对象的元素值,不能直接用于普通对象。
1 | const arr = [10, 20, 30]; |
原则:遍历数组、Map、Set、字符串等,使用 for-of。遍历普通对象属性,使用 for-in 配合 hasOwnProperty 或直接使用 Object.keys/values/entries。
4. 生成器函数:function* 与 yield
生成器函数通过 function* 声明(或 function* name),调用时不立即执行函数体,而是返回一个生成器对象(既是迭代器,也是可迭代对象)。
1 | function* simpleGenerator() { |
yield 表达式暂停函数执行并返回一个值给调用者。调用 next() 恢复执行,直到下一个 yield 或函数结束。yield 表达式的值由下一次 next(arg) 传入的参数决定。
4.1 双向通信:next(arg) 传入值
生成器内部的 yield 表达式可以接收来自外部的值。
1 | function* twoWayGenerator() { |
第一次 next() 启动生成器,不能传参(传了也会被忽略)。第二次 next('Alpha') 将 'Alpha' 赋值给第一个 yield 表达式的结果 a。第三次 next('Beta') 将 'Beta' 赋给 b。
4.2 yield*:委托给另一个可迭代对象
yield* 将生成器内部的控制权委托给另一个可迭代对象(数组、另一个生成器等),逐个产出它的所有值。
1 | function* subGenerator() { |
yield* 也可以将来自调用者的 next(arg) 参数传递给委托的生成器,实现完全透明的委托。
5. 生成器的高级用法
5.1 生成无限序列(惰性求值)
生成器天然支持惰性求值——仅在需要时才计算下一个值。这适合生成无限序列或数据流。
1 | function* fibonacci() { |
5.2 生成器作为自定义迭代器
任何需要自定义遍历逻辑的对象,都可以使用生成器简化迭代器的实现。
1 | const range = { |
使用生成器函数作为 Symbol.iterator,避免了手动维护状态(如 index)和返回 { value, done } 的样板代码。
5.3 异步生成器(简介)
生成器可以与 Promise 结合,用于管理异步流程(常被 async/await 替代)。但在处理异步数据流时,异步生成器(async function* 和 for await...of)提供了优雅的解决方案。异步生成器将在后续深入讲解。
6. 生成器的 return() 与 throw() 方法
生成器对象除了 next() 外,还有两个方法可用于提前终止或注入错误。
gen.return(value):立即终止生成器,返回{ value, done: true },生成器内部的finally块会执行。gen.throw(error):在生成器内部当前暂停的yield位置抛出一个错误,可以在生成器内部try-catch捕获。
1 | function* canTerminate() { |
课后练习
一、概念自测(选择题 / 填空题)
(单选) 一个对象要支持
for-of循环,它必须实现什么方法?
A.next()
B.Symbol.iterator
C.[Symbol.iterator]()
D.@@iterator(单选) 关于生成器函数
function* gen(),以下描述正确的是?
A. 调用gen()立即执行函数体。
B. 生成器函数内部不能使用return。
C. 生成器函数返回一个生成器对象,该对象既是迭代器也是可迭代对象。
D.yield*只能用于委托另一个生成器。(填空) 在生成器内部,
yield表达式本身的值是由下一次调用______时传入的参数决定的。(多选) 以下哪些类型是内置可迭代对象?
A.Array
B.Map
C. 普通对象{}
D.Set
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个生成器函数 range(start, end, step = 1),生成从 start(包含)到 end(不包含)的等差数列,步长为 step。要求如下:
- 使用
function*声明生成器。 - 如果
step为正,start应小于end,否则不产出任何值;如果step为负,start应大于end。如果矛盾,生成器立即结束。 - 生成器内部使用
while循环和yield。 - 编写一个使用示例:用
for-of遍历range(1, 10, 2)并打印每个值;再用展开运算符将range(5, 0, -1)转为数组并打印。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定生成器函数的语法、边界条件处理和示例用法。
课后练习答案
一、概念自测答案
C
- 解析:对象需要实现
[Symbol.iterator]()方法,返回一个迭代器。Symbol.iterator是 well-known symbol,通过计算属性名[Symbol.iterator]调用。
- 解析:对象需要实现
C
- 解析:生成器函数返回生成器对象,该对象既实现了迭代器协议(
next),又实现了可迭代协议(Symbol.iterator返回自身)。A 错误,调用gen()返回生成器,不立即执行;B 错误,生成器内可使用return终止并设置最终value;D 错误,yield*可委托任何可迭代对象。
- 解析:生成器函数返回生成器对象,该对象既实现了迭代器协议(
next(arg)或next- 解析:
yield表达式的值是下一次调用生成器的next(argument)时传入的参数。第一次next()无法传值。
- 解析:
A、B、D
- 解析:Array、Map、Set 都是内置可迭代对象。普通对象
{}默认不是可迭代的,需要手动实现[Symbol.iterator]()才能用于for-of。
- 解析:Array、Map、Set 都是内置可迭代对象。普通对象
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写一个生成器函数range(start, end, step = 1),生成等差数列。要求:
- 使用
function*声明。- 在函数开头检查参数有效性:如果
(step > 0 && start >= end) || (step < 0 && start <= end),则直接return(生成器结束)。- 使用
let current = start; while ((step > 0 && current < end) || (step < 0 && current > end)) { yield current; current += step; }产出值。- 提供一个使用示例:使用
for...of遍历range(1, 10, 2)打印 1,3,5,7,9;使用console.log([...range(5, 0, -1)])打印 [5,4,3,2,1]。- 代码简洁,添加 JSDoc 注释。直接输出完整代码。”