为什么要关心迭代器和生成器?
在 JavaScript 中,大多数人第一次接触生成器可能是在看到 Redux-Saga 或者处理异步流的时候。很多人把它当作"高级语法",觉得用不上就不学了。但实际上,生成器揭示了 JavaScript 中一个非常底层的能力:可以暂停和恢复的函数执行。
这个能力为什么重要?因为它打破了 JavaScript 函数"要么没执行,要么执行完"的二元状态,引入了"中间态"。这个中间态让很多原本不可能的事情变得可能:
- 处理无限序列而不会爆内存
- 用同步的写法处理异步逻辑
- 实现协程式的任务调度
- 构建惰性求值的数据处理管道
而 Rust 语言在迭代器设计上的理念,能帮我们更深刻地理解这些概念。
从一个问题开始:如何遍历不同的数据结构?
假设你有三种用户数据:
const usersArray = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
const usersMap = new Map([
['user5', { id: 5, name: 'Eve' }],
['user6', { id: 6, name: 'Frank' }]
])
const usersSet = new Set([
{ id: 7, name: 'Grace' },
{ id: 8, name: 'Henry' }
])
const usersArray = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
const usersMap = new Map([
['user5', { id: 5, name: 'Eve' }],
['user6', { id: 6, name: 'Frank' }]
])
const usersSet = new Set([
{ id: 7, name: 'Grace' },
{ id: 8, name: 'Henry' }
])
在没有迭代器之前,你需要针对每种数据结构写不同的遍历逻辑:
function processUsers(dataSource, type) {
if (type === 'array') {
for (let i = 0; i < dataSource.length; i++)
console.log(dataSource[i].name)
}
else if (type === 'map') {
dataSource.forEach((user, key) => {
console.log(user.name)
})
}
else if (type === 'set') {
dataSource.forEach((user) => {
console.log(user.name)
})
}
}
function processUsers(dataSource, type) {
if (type === 'array') {
for (let i = 0; i < dataSource.length; i++)
console.log(dataSource[i].name)
}
else if (type === 'map') {
dataSource.forEach((user, key) => {
console.log(user.name)
})
}
else if (type === 'set') {
dataSource.forEach((user) => {
console.log(user.name)
})
}
}
这很烦人。迭代器的核心思想就是:把"如何遍历"的逻辑封装在数据结构内部,外部只需要知道一个统一的接口。
有了迭代器,所有的遍历都变成了:
function processUsers(dataSource) {
for (const user of dataSource.values())
console.log(user.name)
}
function processUsers(dataSource) {
for (const user of dataSource.values())
console.log(user.name)
}
不管你传入的是数组、Map 还是 Set,代码都一样。这就是迭代器模式的价值。
JavaScript 的迭代器协议
JavaScript 定义了两个协议:
1. 迭代器协议(Iterator Protocol)
一个对象只要有 next() 方法,并且这个方法返回 {value, done} 格式的对象,它就是迭代器。
function createRangeIterator(start, end) {
let current = start
return {
next() {
if (current <= end)
return { value: current++, done: false }
return { done: true }
}
}
}
const iter = createRangeIterator(1, 3)
console.log(iter.next()) // { value: 1, done: false }
console.log(iter.next()) // { value: 2, done: false }
console.log(iter.next()) // { value: 3, done: false }
console.log(iter.next()) // { done: true }
function createRangeIterator(start, end) {
let current = start
return {
next() {
if (current <= end)
return { value: current++, done: false }
return { done: true }
}
}
}
const iter = createRangeIterator(1, 3)
console.log(iter.next()) // { value: 1, done: false }
console.log(iter.next()) // { value: 2, done: false }
console.log(iter.next()) // { value: 3, done: false }
console.log(iter.next()) // { done: true }
2. 可迭代协议(Iterable Protocol)
一个对象只要有 [Symbol.iterator]() 方法,并且这个方法返回一个迭代器,它就是可迭代对象。
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start
const end = this.end
return {
next() {
if (current <= end)
return { value: current++, done: false }
return { done: true }
}
}
}
}
// 现在可以用 for...of 了
for (const num of range)
console.log(num) // 1, 2, 3, 4, 5
// 也可以用扩展运算符
console.log([...range]) // [1, 2, 3, 4, 5]
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start
const end = this.end
return {
next() {
if (current <= end)
return { value: current++, done: false }
return { done: true }
}
}
}
}
// 现在可以用 for...of 了
for (const num of range)
console.log(num) // 1, 2, 3, 4, 5
// 也可以用扩展运算符
console.log([...range]) // [1, 2, 3, 4, 5]
注意区别:
- 迭代器(Iterator):有
next()方法的对象,定义了如何获取数据 - 可迭代对象(Iterable):有
[Symbol.iterator]()方法的对象,定义了如何获取迭代器
大多数内置的迭代器(比如数组的迭代器)既是迭代器,也是可迭代对象:
const arr = [1, 2, 3]
const iter = arr[Symbol.iterator]()
console.log(iter.next) // ƒ next() { ... }
console.log(iter[Symbol.iterator]) // ƒ [Symbol.iterator]() { ... }
console.log(iter[Symbol.iterator]() === iter) // true
const arr = [1, 2, 3]
const iter = arr[Symbol.iterator]()
console.log(iter.next) // ƒ next() { ... }
console.log(iter[Symbol.iterator]) // ƒ [Symbol.iterator]() { ... }
console.log(iter[Symbol.iterator]() === iter) // true
这种"迭代器也是可迭代对象"的设计,让迭代器可以被 for...of、... 等语法消费,使用起来更方便。
Rust 中的迭代器:一个对比视角
Rust 的迭代器设计非常相似,但更加严谨。Rust 通过 Iterator trait 定义迭代器:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
使用起来:
let mut iter = 1..=3;
println!("{:?}", iter.next()); // Some(1)
println!("{:?}", iter.next()); // Some(2)
println!("{:?}", iter.next()); // Some(3)
println!("{:?}", iter.next()); // None
let mut iter = 1..=3;
println!("{:?}", iter.next()); // Some(1)
println!("{:?}", iter.next()); // Some(2)
println!("{:?}", iter.next()); // Some(3)
println!("{:?}", iter.next()); // None
对比 JavaScript:
| 特性 | JavaScript | Rust |
|---|---|---|
| 方法签名 | next() -> {value, done} | next(&mut self) -> Option<Item> |
| 类型安全 | 运行时检查 | 编译时保证 |
| 结束表示 | {done: true} | None |
| 可变性 | 隐式可变 | 显式 &mut self |
Rust 的 Option<Item> 更加类型安全:要么有值(Some(value)),要么没有(None),不会出现 JavaScript 中 {value: undefined, done: true} 这种模棱两可的情况。
生成器:迭代器的语法糖
手写迭代器很麻烦,你需要手动管理状态:
// 手写斐波那契迭代器
function createFibIterator() {
let prev = 0; let curr = 1
return {
next() {
const value = curr;
[prev, curr] = [curr, prev + curr]
return { value, done: false }
}
}
}
// 手写斐波那契迭代器
function createFibIterator() {
let prev = 0; let curr = 1
return {
next() {
const value = curr;
[prev, curr] = [curr, prev + curr]
return { value, done: false }
}
}
}
生成器让这一切变得简单:
// 用生成器实现斐波那契
function* fibonacci() {
let prev = 0; let curr = 1
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr]
}
}
const fib = fibonacci()
console.log(fib.next().value) // 1
console.log(fib.next().value) // 1
console.log(fib.next().value) // 2
console.log(fib.next().value) // 3
console.log(fib.next().value) // 5
// 用生成器实现斐波那契
function* fibonacci() {
let prev = 0; let curr = 1
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr]
}
}
const fib = fibonacci()
console.log(fib.next().value) // 1
console.log(fib.next().value) // 1
console.log(fib.next().value) // 2
console.log(fib.next().value) // 3
console.log(fib.next().value) // 5
生成器的执行机制
生成器的神奇之处在于 yield 关键字:
- 调用生成器函数不会立即执行函数体,而是返回一个生成器对象
- 调用
.next()时才开始执行,直到遇到第一个yield yield会暂停函数执行,把yield后面的值返回给外部- 再次调用
.next()时从上次暂停的地方继续执行
function* demo() {
console.log('开始')
yield 1
console.log('继续')
yield 2
console.log('结束')
return 3
}
const gen = demo()
console.log('创建生成器') // 创建生成器
console.log(gen.next()) // 开始, { value: 1, done: false }
console.log(gen.next()) // 继续, { value: 2, done: false }
console.log(gen.next()) // 结束, { value: 3, done: true }
function* demo() {
console.log('开始')
yield 1
console.log('继续')
yield 2
console.log('结束')
return 3
}
const gen = demo()
console.log('创建生成器') // 创建生成器
console.log(gen.next()) // 开始, { value: 1, done: false }
console.log(gen.next()) // 继续, { value: 2, done: false }
console.log(gen.next()) // 结束, { value: 3, done: true }
yield* 委托
yield* 可以把迭代委托给另一个可迭代对象:
function* inner() {
yield 2
yield 3
}
function* outer() {
yield 1
yield * inner() // 委托给 inner
yield 4
}
console.log([...outer()]) // [1, 2, 3, 4]
function* inner() {
yield 2
yield 3
}
function* outer() {
yield 1
yield * inner() // 委托给 inner
yield 4
}
console.log([...outer()]) // [1, 2, 3, 4]
这在处理树结构时特别有用:
function* traverseTree(node) {
if (!node)
return
yield node.value // 访问当前节点
yield * traverseTree(node.left) // 遍历左子树
yield * traverseTree(node.right) // 遍历右子树
}
const tree = {
value: 1,
left: { value: 2, left: null, right: null },
right: { value: 3, left: null, right: null }
}
console.log([...traverseTree(tree)]) // [1, 2, 3]
function* traverseTree(node) {
if (!node)
return
yield node.value // 访问当前节点
yield * traverseTree(node.left) // 遍历左子树
yield * traverseTree(node.right) // 遍历右子树
}
const tree = {
value: 1,
left: { value: 2, left: null, right: null },
right: { value: 3, left: null, right: null }
}
console.log([...traverseTree(tree)]) // [1, 2, 3]
生成器的双向通信
生成器不仅能向外 yield 值,还能接收外部传入的值。这是通过 next(value) 实现的:
function* conversation() {
console.log('开始对话')
const name = yield '你叫什么名字?'
console.log(`收到名字: ${name}`)
const age = yield '你多大了?'
console.log(`收到年龄: ${age}`)
return `${name} 今年 ${age} 岁`
}
const chat = conversation()
console.log(chat.next().value) // 开始对话, "你叫什么名字?"
console.log(chat.next('Alice').value) // 收到名字: Alice, "你多大了?"
console.log(chat.next(25).value) // 收到年龄: 25, "Alice 今年 25 岁"
function* conversation() {
console.log('开始对话')
const name = yield '你叫什么名字?'
console.log(`收到名字: ${name}`)
const age = yield '你多大了?'
console.log(`收到年龄: ${age}`)
return `${name} 今年 ${age} 岁`
}
const chat = conversation()
console.log(chat.next().value) // 开始对话, "你叫什么名字?"
console.log(chat.next('Alice').value) // 收到名字: Alice, "你多大了?"
console.log(chat.next(25).value) // 收到年龄: 25, "Alice 今年 25 岁"
关键理解:next(value) 传入的值会作为上一个 yield 表达式的返回值。
控制流:return() 和 throw()
除了 next(),生成器还提供了 return() 和 throw() 方法:
return(value) - 提前结束生成器:
function* gen() {
try {
yield 1
yield 2
yield 3
}
finally {
console.log('清理资源')
}
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.return(999)) // 清理资源, { value: 999, done: true }
console.log(g.next()) // { value: undefined, done: true }
function* gen() {
try {
yield 1
yield 2
yield 3
}
finally {
console.log('清理资源')
}
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.return(999)) // 清理资源, { value: 999, done: true }
console.log(g.next()) // { value: undefined, done: true }
throw(error) - 向生成器内部抛出错误:
function* gen() {
try {
yield 1
yield 2
}
catch (e) {
console.log('捕获到错误:', e.message)
yield 999
}
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.throw(new Error('出错了'))) // 捕获到错误: 出错了, { value: 999, done: false }
function* gen() {
try {
yield 1
yield 2
}
catch (e) {
console.log('捕获到错误:', e.message)
yield 999
}
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.throw(new Error('出错了'))) // 捕获到错误: 出错了, { value: 999, done: false }
这三个方法(next、return、throw)构成了生成器作为协程的完整控制协议。
惰性求值:生成器的核心优势
理解惰性求值,先看一个对比:
饥饿求值(Eager Evaluation)- 数组方法
const arr = [1, 2, 3, 4, 5]
const result = arr
.map((x) => {
console.log(`map: ${x}`)
return x * 2
})
.filter((x) => {
console.log(`filter: ${x}`)
return x > 5
})
console.log(result)
// 输出:
// map: 1
// map: 2
// map: 3
// map: 4
// map: 5
// filter: 2
// filter: 4
// filter: 6
// filter: 8
// filter: 10
// [6, 8, 10]
const arr = [1, 2, 3, 4, 5]
const result = arr
.map((x) => {
console.log(`map: ${x}`)
return x * 2
})
.filter((x) => {
console.log(`filter: ${x}`)
return x > 5
})
console.log(result)
// 输出:
// map: 1
// map: 2
// map: 3
// map: 4
// map: 5
// filter: 2
// filter: 4
// filter: 6
// filter: 8
// filter: 10
// [6, 8, 10]
注意执行顺序:先把所有元素 map 完,再把所有元素 filter 完。这是批处理模式。
惰性求值(Lazy Evaluation)- 生成器
function* lazySequence() {
for (const x of [1, 2, 3, 4, 5]) {
console.log(`产生: ${x}`)
yield x
}
}
// ES2025 Iterator Helpers
const result = lazySequence()
.map((x) => {
console.log(`map: ${x}`)
return x * 2
})
.filter((x) => {
console.log(`filter: ${x}`)
return x > 5
})
.toArray()
console.log(result)
// 输出:
// 产生: 1
// map: 1
// filter: 2
// 产生: 2
// map: 2
// filter: 4
// 产生: 3
// map: 3
// filter: 6 ← 流水线式处理
// 产生: 4
// map: 4
// filter: 8
// 产生: 5
// map: 5
// filter: 10
// [6, 8, 10]
function* lazySequence() {
for (const x of [1, 2, 3, 4, 5]) {
console.log(`产生: ${x}`)
yield x
}
}
// ES2025 Iterator Helpers
const result = lazySequence()
.map((x) => {
console.log(`map: ${x}`)
return x * 2
})
.filter((x) => {
console.log(`filter: ${x}`)
return x > 5
})
.toArray()
console.log(result)
// 输出:
// 产生: 1
// map: 1
// filter: 2
// 产生: 2
// map: 2
// filter: 4
// 产生: 3
// map: 3
// filter: 6 ← 流水线式处理
// 产生: 4
// map: 4
// filter: 8
// 产生: 5
// map: 5
// filter: 10
// [6, 8, 10]
注意执行顺序:每个元素走完整条处理链路后,再处理下一个元素。这是流水线模式。
为什么这很重要?
1. 内存效率
// 饥饿求值:需要创建中间数组
const arr = Array.from({ length: 1000000 }, (_, i) => i)
const result1 = arr
.map(x => x * 2) // 创建 100万 元素的中间数组
.filter(x => x > 5) // 再创建一个中间数组
// 惰性求值:不创建中间数组
function* range(n) {
for (let i = 0; i < n; i++) yield i
}
const result2 = range(1000000)
.map(x => x * 2) // 不创建中间数组
.filter(x => x > 5)
.toArray()
// 饥饿求值:需要创建中间数组
const arr = Array.from({ length: 1000000 }, (_, i) => i)
const result1 = arr
.map(x => x * 2) // 创建 100万 元素的中间数组
.filter(x => x > 5) // 再创建一个中间数组
// 惰性求值:不创建中间数组
function* range(n) {
for (let i = 0; i < n; i++) yield i
}
const result2 = range(1000000)
.map(x => x * 2) // 不创建中间数组
.filter(x => x > 5)
.toArray()
2. 可以处理无限序列
function* infiniteSequence() {
let i = 0
while (true) yield i++
}
// 这是可以的!
const firstTenEven = infiniteSequence()
.filter(x => x % 2 === 0)
.take(10)
.toArray()
console.log(firstTenEven) // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
function* infiniteSequence() {
let i = 0
while (true) yield i++
}
// 这是可以的!
const firstTenEven = infiniteSequence()
.filter(x => x % 2 === 0)
.take(10)
.toArray()
console.log(firstTenEven) // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
3. 避免不必要的计算
function* expensiveSequence() {
for (let i = 0; i < 1000000; i++) {
console.log(`计算 ${i}`)
yield Math.sqrt(i)
}
}
// 只会计算到找到第一个大于 855 的数
const result = expensiveSequence()
.find(x => x > 855)
console.log(result) // 计算 0, 计算 1, ..., 计算 732177, 855.0005847951217
function* expensiveSequence() {
for (let i = 0; i < 1000000; i++) {
console.log(`计算 ${i}`)
yield Math.sqrt(i)
}
}
// 只会计算到找到第一个大于 855 的数
const result = expensiveSequence()
.find(x => x > 855)
console.log(result) // 计算 0, 计算 1, ..., 计算 732177, 855.0005847951217
Rust 的零成本抽象理念
Rust 的迭代器以"零成本抽象"著称。什么意思呢?看这段代码:
let sum: i32 = (1..=1000000)
.filter(|x| x % 2 == 0)
.map(|x| x * 2)
.sum();
let sum: i32 = (1..=1000000)
.filter(|x| x % 2 == 0)
.map(|x| x * 2)
.sum();
编译器会把它优化成类似这样的代码:
let mut sum = 0;
for x in 1..=1000000 {
if x % 2 == 0 {
sum += x * 2;
}
}
let mut sum = 0;
for x in 1..=1000000 {
if x % 2 == 0 {
sum += x * 2;
}
}
也就是说,使用高级抽象(迭代器链)的性能和手写循环一样好。这就是"零成本"。
JavaScript 能做到吗?不能。因为:
- JavaScript 是动态语言,无法在编译时做这种优化
- 每次
next()调用都是真实的函数调用 - 需要创建迭代结果对象
{value, done}
所以 JavaScript 的生成器是有成本的:
const iterations = 1000000
// 原生循环
console.time('loop')
let sum1 = 0
for (let i = 0; i < iterations; i++)
sum1 += i
console.timeEnd('loop') // ~3ms
// 生成器
console.time('generator')
function* range(n) {
for (let i = 0; i < n; i++) yield i
}
let sum2 = 0
for (const i of range(iterations))
sum2 += i
console.timeEnd('generator') // ~50ms
const iterations = 1000000
// 原生循环
console.time('loop')
let sum1 = 0
for (let i = 0; i < iterations; i++)
sum1 += i
console.timeEnd('loop') // ~3ms
// 生成器
console.time('generator')
function* range(n) {
for (let i = 0; i < n; i++) yield i
}
let sum2 = 0
for (const i of range(iterations))
sum2 += i
console.timeEnd('generator') // ~50ms
那为什么还要用生成器?
因为在很多场景下,抽象带来的收益远超过性能损失:
- 处理大数据集或无限序列时,内存效率是关键
- 代码可读性和可维护性更重要
- 函数式组合能力带来的灵活性
Iterator Helpers API(ES2025)
JavaScript 在 ES2025 引入了 Iterator Helpers,让迭代器拥有了类似 Rust 的方法链:
// 创建一个恰当的迭代器
const iter = Iterator.from([1, 2, 3, 4, 5])
// 链式调用
const result = iter
.map(x => x * 2)
.filter(x => x > 5)
.take(2)
.toArray()
console.log(result) // [6, 8]
// 创建一个恰当的迭代器
const iter = Iterator.from([1, 2, 3, 4, 5])
// 链式调用
const result = iter
.map(x => x * 2)
.filter(x => x > 5)
.take(2)
.toArray()
console.log(result) // [6, 8]
常用方法:
function* range(start, end) {
for (let i = start; i < end; i++) yield i
}
// map - 映射
range(1, 5).map(x => x * 2)
// 2, 4, 6, 8
// filter - 过滤
range(1, 10).filter(x => x % 2 === 0)
// 2, 4, 6, 8
// take - 取前 n 个
range(1, 100).take(5)
// 1, 2, 3, 4, 5
// drop - 跳过前 n 个
range(1, 10).drop(5)
// 6, 7, 8, 9
// reduce - 归约
range(1, 5).reduce((acc, x) => acc + x, 0)
// 10
// find - 查找
range(1, 100).find(x => x > 50)
// 51
// toArray - 转换为数组
range(1, 5).toArray()
// [1, 2, 3, 4]
function* range(start, end) {
for (let i = start; i < end; i++) yield i
}
// map - 映射
range(1, 5).map(x => x * 2)
// 2, 4, 6, 8
// filter - 过滤
range(1, 10).filter(x => x % 2 === 0)
// 2, 4, 6, 8
// take - 取前 n 个
range(1, 100).take(5)
// 1, 2, 3, 4, 5
// drop - 跳过前 n 个
range(1, 10).drop(5)
// 6, 7, 8, 9
// reduce - 归约
range(1, 5).reduce((acc, x) => acc + x, 0)
// 10
// find - 查找
range(1, 100).find(x => x > 50)
// 51
// toArray - 转换为数组
range(1, 5).toArray()
// [1, 2, 3, 4]
这些方法都是惰性的(除了 reduce、toArray 等消费方法),只有在真正消费时才会执行。
实战应用
1. 处理大型数据集
async function* fetchLargeDataset(url) {
let page = 1
let hasMore = true
while (hasMore) {
const response = await fetch(`${url}?page=${page}`)
const data = await response.json()
yield * data.items
hasMore = data.hasMore
page++
}
}
// 使用:只加载需要的数据
for await (const item of fetchLargeDataset('/api/products')) {
console.log(item)
if (item.id === 'target-id')
break // 找到了就停止,不会加载所有页面
}
async function* fetchLargeDataset(url) {
let page = 1
let hasMore = true
while (hasMore) {
const response = await fetch(`${url}?page=${page}`)
const data = await response.json()
yield * data.items
hasMore = data.hasMore
page++
}
}
// 使用:只加载需要的数据
for await (const item of fetchLargeDataset('/api/products')) {
console.log(item)
if (item.id === 'target-id')
break // 找到了就停止,不会加载所有页面
}
2. 实现状态机
function* trafficLight() {
while (true) {
yield 'green'
yield 'yellow'
yield 'red'
}
}
const light = trafficLight()
setInterval(() => {
console.log(light.next().value)
}, 1000)
function* trafficLight() {
while (true) {
yield 'green'
yield 'yellow'
yield 'red'
}
}
const light = trafficLight()
setInterval(() => {
console.log(light.next().value)
}, 1000)
3. 树结构遍历
const tree = {
value: 1,
children: [
{
value: 2,
children: [
{ value: 4, children: [] },
{ value: 5, children: [] }
]
},
{
value: 3,
children: [
{ value: 6, children: [] }
]
}
]
}
// 深度优先遍历
function* dfs(node) {
yield node.value
for (const child of node.children)
yield * dfs(child)
}
// 广度优先遍历
function* bfs(root) {
const queue = [root]
while (queue.length > 0) {
const node = queue.shift()
yield node.value
queue.push(...node.children)
}
}
console.log([...dfs(tree)]) // [1, 2, 4, 5, 3, 6]
console.log([...bfs(tree)]) // [1, 2, 3, 4, 5, 6]
const tree = {
value: 1,
children: [
{
value: 2,
children: [
{ value: 4, children: [] },
{ value: 5, children: [] }
]
},
{
value: 3,
children: [
{ value: 6, children: [] }
]
}
]
}
// 深度优先遍历
function* dfs(node) {
yield node.value
for (const child of node.children)
yield * dfs(child)
}
// 广度优先遍历
function* bfs(root) {
const queue = [root]
while (queue.length > 0) {
const node = queue.shift()
yield node.value
queue.push(...node.children)
}
}
console.log([...dfs(tree)]) // [1, 2, 4, 5, 3, 6]
console.log([...bfs(tree)]) // [1, 2, 3, 4, 5, 6]
4. 数据处理管道
// 日志分析
async function* readLogFile(path) {
const file = await fs.readFile(path, 'utf-8')
const lines = file.split('\n')
yield * lines
}
function* parseLog(lines) {
for (const line of lines) {
const match = line.match(/\[(\w+)\] (.+)/)
if (match)
yield { level: match[1], message: match[2] }
}
}
// 使用
const logs = await readLogFile('app.log')
const errors = parseLog(logs)
.filter(log => log.level === 'ERROR')
.map(log => log.message)
.toArray()
console.log(errors)
// 日志分析
async function* readLogFile(path) {
const file = await fs.readFile(path, 'utf-8')
const lines = file.split('\n')
yield * lines
}
function* parseLog(lines) {
for (const line of lines) {
const match = line.match(/\[(\w+)\] (.+)/)
if (match)
yield { level: match[1], message: match[2] }
}
}
// 使用
const logs = await readLogFile('app.log')
const errors = parseLog(logs)
.filter(log => log.level === 'ERROR')
.map(log => log.message)
.toArray()
console.log(errors)
常见陷阱
1. 生成器是一次性的
function* gen() {
yield 1
yield 2
yield 3
}
const g = gen()
console.log([...g]) // [1, 2, 3]
console.log([...g]) // [] - 已经耗尽了!
// 正确做法:每次创建新的生成器
console.log([...gen()]) // [1, 2, 3]
console.log([...gen()]) // [1, 2, 3]
function* gen() {
yield 1
yield 2
yield 3
}
const g = gen()
console.log([...g]) // [1, 2, 3]
console.log([...g]) // [] - 已经耗尽了!
// 正确做法:每次创建新的生成器
console.log([...gen()]) // [1, 2, 3]
console.log([...gen()]) // [1, 2, 3]
2. return 不会被 for…of 捕获
function* gen() {
yield 1
yield 2
return 3 // 注意这里是 return
}
console.log([...gen()]) // [1, 2] - return 的值丢失了
// 只有手动调用 next() 才能看到
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: true }
function* gen() {
yield 1
yield 2
return 3 // 注意这里是 return
}
console.log([...gen()]) // [1, 2] - return 的值丢失了
// 只有手动调用 next() 才能看到
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: true }
3. 避免副作用
// ❌ 不好的做法
let count = 0
function* bad() {
count++ // 副作用
yield count
}
const g1 = bad()
const g2 = bad()
g1.next() // count = 1
g2.next() // count = 2 (受 g1 影响)
// ✅ 好的做法
function* good() {
let localCount = 0
while (true)
yield ++localCount
}
const g3 = good()
const g4 = good()
g3.next() // 1
g4.next() // 1 (独立)
// ❌ 不好的做法
let count = 0
function* bad() {
count++ // 副作用
yield count
}
const g1 = bad()
const g2 = bad()
g1.next() // count = 1
g2.next() // count = 2 (受 g1 影响)
// ✅ 好的做法
function* good() {
let localCount = 0
while (true)
yield ++localCount
}
const g3 = good()
const g4 = good()
g3.next() // 1
g4.next() // 1 (独立)
4. React 中不要直接消费生成器
// ❌ 错误
function* range() {
for (let i = 0; i < 10; i++) yield i;
}
const seq = range();
function Component() {
return (
<div>
{[...seq].map(n => <span key={n}>{n}</span>)}
</div>
);
}
// 第一次渲染:[0, 1, 2, ..., 9]
// 第二次渲染:[] - seq 已经耗尽了
// ✅ 正确
function Component() {
const data = useMemo(() => [...range()], []);
return (
<div>
{data.map(n => <span key={n}>{n}</span>)}
</div>
);
}
// 或者直接传数组
const data = [...range()];
function Component() {
return (
<div>
{data.map(n => <span key={n}>{n}</span>)}
</div>
);
}
// ❌ 错误
function* range() {
for (let i = 0; i < 10; i++) yield i;
}
const seq = range();
function Component() {
return (
<div>
{[...seq].map(n => <span key={n}>{n}</span>)}
</div>
);
}
// 第一次渲染:[0, 1, 2, ..., 9]
// 第二次渲染:[] - seq 已经耗尽了
// ✅ 正确
function Component() {
const data = useMemo(() => [...range()], []);
return (
<div>
{data.map(n => <span key={n}>{n}</span>)}
</div>
);
}
// 或者直接传数组
const data = [...range()];
function Component() {
return (
<div>
{data.map(n => <span key={n}>{n}</span>)}
</div>
);
}
何时使用生成器?
简单的决策:
- 处理大数据集或无限序列 → 使用生成器
- 需要惰性求值 → 使用生成器
- 需要暂停/恢复的控制流(状态机、任务调度)→ 使用生成器
- 函数式数据转换管道 → 使用生成器 + Iterator Helpers
- 简单的数组操作 → 直接用数组方法
- 性能关键路径 → 用原生循环
从 Rust 学到的设计思想
虽然 JavaScript 无法实现 Rust 的零成本抽象,但我们可以学习它的设计理念:
1. 类型安全
用 TypeScript 增强类型安全:
function* range(start: number, end: number): Generator<number> {
for (let i = start; i < end; i++) {
yield i;
}
}
function* map<T, U>(
iter: Iterable<T>,
fn: (item: T) => U
): Generator<U> {
for (const item of iter) {
yield fn(item);
}
}
function* range(start: number, end: number): Generator<number> {
for (let i = start; i < end; i++) {
yield i;
}
}
function* map<T, U>(
iter: Iterable<T>,
fn: (item: T) => U
): Generator<U> {
for (const item of iter) {
yield fn(item);
}
}
2. 所有权思维
生成器是一次性的,这类似 Rust 的所有权转移:
const gen = fibonacci()
const first10 = [...take(gen, 10)]
// gen 现在已经前进了 10 步,不能"重放"
const gen = fibonacci()
const first10 = [...take(gen, 10)]
// gen 现在已经前进了 10 步,不能"重放"
3. 组合优于继承
构建小的、可组合的迭代器工具:
function* map(iter, fn) {
for (const item of iter) yield fn(item)
}
function* filter(iter, pred) {
for (const item of iter) {
if (pred(item))
yield item
}
}
function* take(iter, n) {
let count = 0
for (const item of iter) {
if (count++ >= n)
break
yield item
}
}
// 组合使用
const result = [
...take(
filter(
map(fibonacci(), x => x * 2),
x => x > 100
),
5
)
]
function* map(iter, fn) {
for (const item of iter) yield fn(item)
}
function* filter(iter, pred) {
for (const item of iter) {
if (pred(item))
yield item
}
}
function* take(iter, n) {
let count = 0
for (const item of iter) {
if (count++ >= n)
break
yield item
}
}
// 组合使用
const result = [
...take(
filter(
map(fibonacci(), x => x * 2),
x => x > 100
),
5
)
]
总结
迭代器和生成器是 JavaScript 中被低估的特性。它们不仅仅是语法糖,而是揭示了语言的深层能力:
- 迭代器统一了遍历接口,让不同数据结构可以用同样的方式遍历
- 生成器引入了可暂停/恢复的函数执行,实现了协程的基础
- 惰性求值让我们可以处理无限序列,节省内存
- 函数式组合让代码更清晰、更易维护
Rust 的设计理念给了我们很好的启发:虽然 JavaScript 无法做到零成本抽象,但在合适的场景下,抽象带来的收益远超过性能损失。
下次当你需要处理数据流、实现状态机或者构建数据处理管道时,不妨试试生成器——你可能会发现一个全新的世界。