第7章 7.4 迭代器与生成器

🎯 开场 3 分钟:为什么要学这个?

上一章我们学会了用 Proxy 劫持对象操作、用 Reflect 做反向操作,学完感觉「哎,JavaScript 底层尽在掌握」。但你有没有遇到过这种情况:

  • 读取一个大文件,一行一行处理,结果内存爆了
  • 写了个函数返回一堆数据,结果用 for 循环居然没法直接用
  • 想做个「懒加载」的数据流,数据用到才生成,不用提前算好

这些问题,「迭代器」和「生成器」能一句话回答你:「数据要多少,我给你产多少。」

今天这章,教你彻底搞懂这对兄弟,让你的代码更「懒」——该偷懒的地方绝不勤奋。


🧱 基础 25 分钟:核心概念

先理解「 iterable 」和「 iterator 」的区别

打个比方:

  • iterable 是一本菜谱(你可以翻页)
  • iterator 是你翻到第几页的书签

菜谱本身不重要,重要的是你能一页一页翻。

在 JavaScript 里,Arra\n\n![Simple tech illustration expla](https://blog.xxyye.com/wp-content/uploads/2026/06/38d5d0e3bce0a19.png)\n\n![AI comic creation scene, creat](https://blog.xxyye.com/wp-content/uploads/2026/06/2113f0225e9b10f.png)\n\nyStringMapSet 都是 iterable,但它们本身不是 iterator——你得调用 Symbol.iterator() 才能拿到那个「书签」。

// 数组是可迭代的
const fruits = ['苹果', '香蕉', '橙子']

// 获取迭代器(书签)
const bookmark = fruits[Symbol.iterator]()

// 第一次翻页
console.log(bookmark.next())  // { value: '苹果', done: false }

// 第二次翻页
console.log(bookmark.next())  // { value: '香蕉', done: false }

// 第三次翻页
console.log(bookmark.next())  // { value: '橙子', done: false }

// 第四次翻页,没内容了
console.log(bookmark.next())  // { value: undefined, done: true }

next() 返回的对象里,value 是当前值,done 表示「翻完了没」。

为什么需要迭代器?

想象你有个需求:读取一个 1GB 的日志文件,统计「ERROR」出现的次数。

传统做法:一次性读取 1GB 到内存,爆了。

迭代器做法:读一行,处理一行,内存占用恒定。

// 这是一个假的日志生成器,模拟读大文件
function* readLinesFake(totalLines) {
for (let i = 1; i <= totalLines; i++) {
yield `第 ${i} 行日志内容`
}
}

const logIterator = readLinesFake(1000000)  // 一百万行
let errorCount = 0

// 只处理前 100 行看看
for (let i = 0; i < 100; i++) {
const line = logIterator.next().value
if (line.includes('ERROR')) errorCount++
}

console.log(`前100行里有 ${errorCount} 个ERROR`)
// 输出:前100行里有 0 个ERROR(因为我们模拟的数据没有ERROR)

你看,处理一百万行数据,但内存只占用「当前这一行」的空间。

生成器函数:写迭代器的偷懒神器

普通迭代器写起来麻烦,生成器帮你简化:

// 普通迭代器写法
const countdown = {
current: 3,
next() {
if (this.current > 0) {
  return { value: this.current--, done: false }
}
return { value: undefined, done: true }
}
}

// 生成器写法 - 一行顶上面十几行
function* countdownGenerator() {
yield 3
yield 2
yield 1
yield '发射!'
}

const countdown = countdownGenerator()

console.log(countdown.next().value)  // 3
console.log(countdown.next().value)  // 2
console.log(countdown.next().value)  // 1
console.log(countdown.next().value)  // 发射!

function* 声明是生成器函数,yield 是「让位」关键字——执行到 yield 就暂停,把值传给调用方,下次调用再从暂停处继续。

yield 可以传值,也可以收值

这是生成器最骚的功能之一:

function* authSystem() {
const username = yield '请输入用户名'
const password = yield '请输入密码'
return `登录成功:${username}`
}

const auth = authSystem()

console.log(auth.next().value)           // 请输入用户名
console.log(auth.next('小明').value)      // 请输入密码
console.log(auth.next('123456').value)   // 登录成功:小明

第一次 next() 停在第一个 yield,返回提示语。第二次传入 '小明' 作为第一个 yield 的值,以此类推。


🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):自定义一个可迭代对象

做一个「计数器」,可以被 for...of 直接遍历:

// 需求:实现一个数字范围,可以 for...of 遍历
class Range {
constructor(start, end) {
this.start = start
this.end = end
}

// 关键:添加这个方法,让对象变成可迭代的
[Symbol.iterator]() {
let current = this.start
const end = this.end

return {
  next() {
    if (current <= end) {
      return { value: current++, done: false }
    }
    return { value: undefined, done: true }
  }
}
}
}

// 使用
for (const num of new Range(1, 5)) {
console.log(num)
}
// 输出:1 2 3 4 5

一句话解释:加了 [Symbol.iterator]() 方法后,for...of 就知道怎么「翻页」了。


项目 2(15 分钟):从 JSON 数据流中提取信息

模拟一个场景:从 API 获取用户数据,但 API 分页返回,用生成器懒加载处理:

// 模拟分页 API 返回
function* fetchUsersPage(pageNum) {
const allUsers = [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 },
{ id: 3, name: '王五', age: 28 },
{ id: 4, name: '赵六', age: 35 },
{ id: 5, name: '钱七', age: 22 },
]

const pageSize = 2
const start = pageNum * pageSize
const end = start + pageSize

// 模拟网络延迟
yield { type: 'loading', page: pageNum }

const pageUsers = allUsers.slice(start, end)
yield { type: 'data', users: pageUsers, hasMore: end < allUsers.length }
}

// 分页获取所有用户
async function getAllUsers() {
let page = 0
const allUsers = []

while (true) {
const result = await new Promise(resolve => {
  setTimeout(() => {
    const gen = fetchUsersPage(page)
    resolve(gen.next().value)
  }, 500)
})

console.log(`第 ${page + 1} 页返回:`, result)

if (result.type === 'data') {
  allUsers.push(...result.users)
  if (!result.hasMore) break
  page++
}
}

return allUsers
}

// 测试
getAllUsers().then(users => {
console.log('所有用户:', users.map(u => u.name).join(', '))
})

预期输出

第 1 页返回: { type: 'loading', page: 0 }
第 1 页返回: { type: 'data', users: [...], hasMore: true }
第 2 页返回: { type: 'loading', page: 1 }
第 2 页返回: { type: 'data', users: [...], hasMore: true }
第 3 页返回: { type: 'loading', page: 2 }
第 3 页返回: { type: 'data', users: [...], hasMore: false }
所有用户: 张三, 李四, 王五, 赵六, 钱七

一句话解释:生成器让我们能「暂停」在每个数据获取点,方便插入 loading 状态或做错误处理。


项目 3(15 分钟):做一个数据清洗流水线

把原始数据「流水线」处理,过滤、空值填充、格式化一条龙:

// 原始数据(可能有各种问题)
const rawData = [
{ name: '  张三  ', age: '25', email: 'zhangsan@example.com', phone: '' },
{ name: '李四', age: 30, email: null, phone: '13800138000' },
{ name: '王五', age: '28', email: 'wangwu@example.com', phone: '13900139000' },
{ name: '', age: 0, email: 'invalid-email', phone: '' },
]

// 步骤1:过滤掉无效数据
function* filterInvalid(records) {
for (const record of records) {
if (!record.name || record.name.trim() === '') continue  // 名字为空跳过
if (!record.email || !record.email.includes('@')) continue  // 邮箱无效跳过
yield record
}
}

// 步骤2:清洗和格式化
function* cleanRecords(records) {
for (const record of records) {
yield {
  name: record.name.trim(),
  age: parseInt(record.age) || 0,
  email: record.email,
  phone: record.phone || '未填写',
  registered: true
}
}
}

// 步骤3:格式化输出
function* formatOutput(records) {
for (const record of records) {
yield `[${record.name}] ${record.age}岁 | ${record.email} | 电话:${record.phone}`
}
}

// 组合流水线
function* dataPipeline(rawRecords) {
const filtered = filterInvalid(rawRecords)
const cleaned = cleanRecords(filtered)
yield* formatOutput(cleaned)  // yield* 把另一个生成器的值yield出去
}

// 运行
console.log('=== 清洗后的数据 ===')
for (const line of dataPipeline(rawData)) {
console.log(line)
}

预期输出

=== 清洗后的数据 ===
[张三] 25岁 | zhangsan@example.com | 电话:未填写
[李四] 30岁 | 未填写 | 电话:13800138000
[王五] 28岁 | wangwu@example.com | 电话:13900139000

一句话解释yield* 像管道接头,把多个生成器串成一条流水线。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:生成器只执行一次

// ❌ 错误:以为可以反复使用
function* numbers() {
yield 1
yield 2
}

const gen = numbers()
console.log([...gen])  // [1, 2]
console.log([...gen])  // []  第二次是空的!

// ✅ 正确:需要重新创建
function* numbers() {
yield 1
yield 2
}
console.log([...numbers()])  // [1, 2]
console.log([...numbers()])  // [1, 2]

坑 2:yield 优先级问题

// ❌ 错误:以为会返回 6
function* wrong() {
return yield 1 + 2  // yield 1 + 2 先执行,然后 return
}

// ✅ 正确:加括号
function* correct() {
return (yield 1) + 2
}

坑 3:for...of 取不到 return 的值

function* gen() {
yield 1
yield 2
return 'done'  // 这个值 for...of 拿不到
}

for (const v of gen()) {
console.log(v)  // 1, 2
}
console.log('最后的值:', gen().next().value)  // done

坑 4:生成器不是异步的

// ❌ 错误:以为 yield 会等 Promise
function* wrong() {
const data = yield fetch('/api/data')
console.log(data)  // Promise,不会等你
}

// ✅ 正确:配合 async/await 用
async function* correct() {
const data = await fetch('/api/data')
console.log(data)
}

性能小贴士:用生成器处理大数据集

// 如果你有一个 100 万条数据的数组
const bigArray = Array.from({ length: 1000000 }, (_, i) => i)

// ❌ 低效:一次性处理全部
const result1 = bigArray.filter(x => x % 2 === 0).map(x => x * 2)

// ✅ 高效:生成器链式处理,每次只占用一个元素的内存
function* processData(data) {
for (const item of data) {
if (item % 2 === 0) {
  yield item * 2
}
}
}

调试技巧:用展开查看状态

function* debugDemo() {
let step = 0
yield step++  // 0
yield step++  // 1
yield step++  // 2
}

const gen = debugDemo()
const first = gen.next()
console.log('第一个状态:', first, ',剩余:', [...gen])
// 输出:第一个状态: { value: 0, done: false },剩余: [1, 2]

✏️ 练习题 + 作业题

练习题(5 道,10 分钟内完成)

练习 1(2 分钟):让字符串变成可迭代对象
- 输入:'hello'
- 预期输出:h e l l o(每个字符一行)
- 提示:'hello'[Symbol.iterator]() 返回什么?


练习 2(2 分钟):在项目 1 的 Range 类加个步长
- 输入:new Range(0, 10, 2),遍历输出
- 预期输出:0 2 4 6 8 10
- 提示:给 Range 构造函数加个 step 参数


练习 3(2 分钟):用生成器实现斐波那契数列前 10 项
- 输入:for (const n of fibonacci(10)) console.log(n)
- 预期输出:1 1 2 3 5 8 13 21 34 55
- 提示:每次 yield 后更新两个变量


练习 4(3 分钟):把练习 3 的斐波那契和项目 2 的分页结合
- 需求:斐波那契数列也要分页返回
- 预期输出:每页返回 3 个斐波那契数
- 提示:外层循环控制页,内层循环控制每页数量


练习 5(3 分钟):看报错分析原因

function* broken() {
yield 1
return 'finished'
}

const result = [...broken()]
console.log(result)  // 输出是什么?为什么?
  • 预期输出:[1](不是 [1, 'finished']
  • 提示:for...of 不包含 return 的值

作业题(30 分钟-2 小时)

作业:做一个「数据流处理工具箱」

需求描述:做一个命令行小工具,可以从标准输入读取多行数据,经过一系列「管道」处理后输出。

功能点:
1. 支持从数组模拟输入(方便测试)
2. 实现以下管道操作:
filter(predicate) - 过滤数据
map(transform) - 转换数据
take(n) - 只取前 n 条
takeWhile(predicate) - 满足条件时继续取,遇到不满足就停
3. 链式调用,如 pipe(data).filter(x => x > 0).map(x => x * 2).take(3)

加分项:
1. 支持 unique() 去重
2. 支持 reduce() 汇总

验收标准:
- 完整可运行(Node.js 环境)
- 代码有注释
- 至少展示 3 个不同管道的使用例子


📚 总结 + 资源

本文学到的 3 个核心点:

  1. Iterable(可迭代对象) — 有 Symbol.iterator 方法的对象,for...of 能遍历它
  2. Iterator(迭代器) — 有 next() 方法的对象,返回 {value, done},控制遍历进度
  3. Generator(生成器) — 用 function*yield 简化迭代器写法,支持「暂停-继续」,是实现「懒加载」的神器

延伸学习资源:

  • MDN 迭代器与生成器文档 — 官方权威资料
  • 《你不知道的 JavaScript》async 与性能篇 — 进阶必读
  • 视频:【硬核】10 分钟搞懂迭代器协议 — 配合本文观看效果更佳

互动钩子:

下一章我们要做一个超有意思的综合实战——「响应式数据绑定迷你 Vue」。没错,就是那个让你又爱又头疼的 Vue 双向绑定背后,藏着迭代器和生成器的身影。剧透一下:学完下一章,你也能写出一个简化版 Vue 的响应式系统!🚀


你在处理大数据时踩过内存的坑吗?有没有用过「懒加载」的场景?评论区聊聊,帮你出主意!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。