第7章 7.4 迭代器与生成器
🎯 开场 3 分钟:为什么要学这个?
上
上一章我们学会了用 Proxy 劫持对象操作、用 Reflect 做反向操作,学完感觉「哎,JavaScript 底层尽在掌握」。但你有没有遇到过这种情况:
- 读取一个大文件,一行一行处理,结果内存爆了
- 写了个函数返回一堆数据,结果用
for循环居然没法直接用 - 想做个「懒加载」的数据流,数据用到才生成,不用提前算好
这些问题,「迭代器」和「生成器」能一句话回答你:「数据要多少,我给你产多少。」
今天这章,教你彻底搞懂这对兄弟,让你的代码更「懒」——该偷懒的地方绝不勤奋。
🧱 基础 25 分钟:核心概念
先理解「 iterable 」和「 iterator 」的区别
打个比方:
- iterable 是一本菜谱(你可以翻页)
- iterator 是你翻到第几页的书签
菜谱本身不重要,重要的是你能一页一页翻。
在 JavaScript 里,Arra\n\n\n\n\n\ny、String、Map、Set 都是 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 个核心点:
- Iterable(可迭代对象) — 有
Symbol.iterator方法的对象,for...of能遍历它 - Iterator(迭代器) — 有
next()方法的对象,返回{value, done},控制遍历进度 - Generator(生成器) — 用
function*和yield简化迭代器写法,支持「暂停-继续」,是实现「懒加载」的神器
延伸学习资源:
- MDN 迭代器与生成器文档 — 官方权威资料
- 《你不知道的 JavaScript》async 与性能篇 — 进阶必读
- 视频:【硬核】10 分钟搞懂迭代器协议 — 配合本文观看效果更佳
互动钩子:
下一章我们要做一个超有意思的综合实战——「响应式数据绑定迷你 Vue」。没错,就是那个让你又爱又头疼的 Vue 双向绑定背后,藏着迭代器和生成器的身影。剧透一下:学完下一章,你也能写出一个简化版 Vue 的响应式系统!🚀
你在处理大数据时踩过内存的坑吗?有没有用过「懒加载」的场景?评论区聊聊,帮你出主意!

评论(0)