© Bohua Xu

理解 JavaScript 迭代器与生成器

Aug 18 · 10min

为什么要关心迭代器和生成器?

在 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:

特性JavaScriptRust
方法签名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 关键字:

  1. 调用生成器函数不会立即执行函数体,而是返回一个生成器对象
  2. 调用 .next() 时才开始执行,直到遇到第一个 yield
  3. yield 会暂停函数执行,把 yield 后面的值返回给外部
  4. 再次调用 .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 }

这三个方法(nextreturnthrow)构成了生成器作为协程的完整控制协议。

惰性求值:生成器的核心优势

理解惰性求值,先看一个对比:

饥饿求值(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 能做到吗?不能。因为:

  1. JavaScript 是动态语言,无法在编译时做这种优化
  2. 每次 next() 调用都是真实的函数调用
  3. 需要创建迭代结果对象 {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]

这些方法都是惰性的(除了 reducetoArray 等消费方法),只有在真正消费时才会执行。

实战应用

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 无法做到零成本抽象,但在合适的场景下,抽象带来的收益远超过性能损失。

下次当你需要处理数据流、实现状态机或者构建数据处理管道时,不妨试试生成器——你可能会发现一个全新的世界。

参考资源

CC BY-NC-SA 4.0 2021-PRESENT © Bohua Xu