欢迎光临

JavaScript迭代器与生成器深度实战:协议原理、自定义迭代、异步生成器与工程化应用完整指南

JavaScript的迭代器(Iterator)和生成器(Generator)是语言中最为精妙却常被低估的特性之一。它们不仅是

1
for...of

循环和展开运算符的底层支撑,更提供了一种优雅的方式来处理懒加载、无限序列、协程式状态机以及异步数据流。本文将从迭代协议的本质出发,逐步深入到生成器函数的高级用法、异步迭代器的工程实践,以及性能优化的关键细节。

一、迭代协议:可迭代对象与迭代器的本质

JavaScript定义了两套协议来统一所有可遍历结构的行为:可迭代协议(Iterable Protocol)和迭代器协议(Iterator Protocol)。理解它们的区别是掌握整个迭代体系的基石。

1.1 可迭代协议(Iterable Protocol)

一个对象只要实现了

1
Symbol.iterator

方法,就被称为”可迭代对象”。该方法必须返回一个符合迭代器协议的对象。这是

1
for...of

、展开运算符

1
...

1
Array.from()

1
解构赋值

等语法糖统一依赖的接口:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const myIterable = {
  data: [10, 20, 30],
  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;
    return {
      next() {
        if (index < data.length) {
          return { value: data[index++], done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

// 以下所有语法都能正常工作
for (const item of myIterable) {
  console.log(item); // 10, 20, 30
}
const [first, ...rest] = myIterable;  // first=10, rest=[20,30]
const copy = [...myIterable];          // [10, 20, 30]

1.2 迭代器协议(Iterator Protocol)

迭代器协议只要求对象实现一个

1
next()

方法,每次调用返回

1
{ value, done }

形式的对象。这是最底层的协议,任何满足此接口的对象都是迭代器:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 最简迭代器:一个计数器
function createCounter(max) {
  let count = 0;
  return {
    next() {
      if (count < max) {
        return { value: count++, done: false };
      }
      return { value: undefined, done: true };
    }
  };
}

const counter = createCounter(3);
console.log(counter.next()); // { value: 0, done: false }
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: undefined, done: true }

关键细节:迭代器不等于可迭代对象。一个纯迭代器不能直接用于

1
for...of

,除非它同时实现了

1
Symbol.iterator

方法(返回自身)。这正是内置迭代器的做法:


1
2
3
4
5
const arr = [1, 2, 3];
const iter = arr[Symbol.iterator]();

// 内置迭代器同时实现了 Symbol.iterator,返回自身
console.log(iter[Symbol.iterator]() === iter); // true

二、生成器函数:用同步语法编写惰性序列

生成器函数是JavaScript对迭代协议最优雅的封装。通过

1
function*

声明和

1
yield

关键字,你可以用看起来像同步代码的方式编写惰性求值的序列:

2.1 基础用法与执行模型


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* fibonacci() {
  let prev = 0, curr = 1;
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

// 取前10个斐波那契数
const fib = fibonacci();
const first10 = [...Array(10)].map(() => fib.next().value);
console.log(first10); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

// 无限序列也可以安全遍历(配合 for...of + break)
for (const n of fibonacci()) {
  if (n > 100) break;
  console.log(n);
}

生成器的执行模型与普通函数截然不同。每次调用

1
next()

时,函数体从上一次

1
yield

暂停的位置恢复执行,直到遇到下一个

1
yield

1
return

。这种”暂停-恢复”机制让生成器天然适合处理不需要一次性加载全部数据的场景。

2.2 yield* 委托:组合多个迭代源

1
yield*

表达式将迭代委托给另一个可迭代对象或生成器。这是构建复杂迭代管道的核心手段:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function* flatMap(iterable, mapper) {
  for (const item of iterable) {
    yield* mapper(item);  // 委托给 mapper 返回的迭代器
  }
}

// 嵌套数组的展平
const nested = [[1, 2], [3, 4], [5]];
const flattened = [...flatMap(nested, arr => arr)];
console.log(flattened); // [1, 2, 3, 4, 5]

// 更实用的场景:树形结构遍历
function* traverseTree(node) {
  yield node.value;
  if (node.children) {
    for (const child of node.children) {
      yield* traverseTree(child);
    }
  }
}

const tree = {
  value: 'root',
  children: [
    { value: 'A', children: [{ value: 'A1' }, { value: 'A2' }] },
    { value: 'B' }
  ]
};
console.log([...traverseTree(tree)]); // ['root', 'A', 'A1', 'A2', 'B']

2.3 双向通信:next()传参与生成器内部逻辑

生成器不仅是数据的生产者,还能通过

1
next(value)

接收外部传入的值,实现双向通信。这一特性是生成器作为协程(coroutine)的基础:


1
2
3
4
5
6
7
8
9
10
function* interactiveProcessor() {
  const name = yield '请输入你的名字';
  const age = yield `你好,${name}!请输入你的年龄`;
  yield `${name},你今年${age}岁,欢迎!`;
}

const proc = interactiveProcessor();
console.log(proc.next().value);      // '请输入你的名字'
console.log(proc.next('张三').value); // '你好,张三!请输入你的年龄'
console.log(proc.next('28').value);   // '张三,你今年28岁,欢迎!'

需要注意一个常见的陷阱:第一次

1
next()

调用传入的值会被忽略,因为生成器函数体还没有执行到任何

1
yield

表达式来接收它。第一次调用只是让函数执行到第一个

1
yield

处。

三、自定义迭代器实战:构建工程级数据结构

虽然生成器简化了大部分迭代器的编写,但在需要精细控制迭代行为(如缓存、索引随机访问、可重置)的场景下,手写迭代器仍然是必要的。

3.1 实现一个支持双向遍历的链表


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }

  append(value) {
    const node = { value, prev: null, next: null };
    if (!this.head) {
      this.head = this.tail = node;
    } else {
      node.prev = this.tail;
      this.tail.next = node;
      this.tail = node;
    }
    this.size++;
    return this;
  }

  // 正向迭代
  [Symbol.iterator]() {
    let current = this.head;
    return {
      next: () => {
        if (current) {
          const value = current.value;
          current = current.next;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }

  // 反向迭代(使用独立方法)
  reverse() {
    let current = this.tail;
    return {
      [Symbol.iterator]() { return this; },
      next: () => {
        if (current) {
          const value = current.value;
          current = current.prev;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const list = new DoublyLinkedList();
list.append(1).append(2).append(3);
console.log([...list]);            // [1, 2, 3]
console.log([...list.reverse()]);   // [3, 2, 1]

3.2 实现一个分页数据迭代器

在实际项目中,我们经常需要遍历分页API的数据。手写一个分页迭代器可以让调用方用

1
for...of

无缝遍历所有页的数据,而不用关心分页逻辑:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class PaginatedIterable {
  constructor(fetchPage, pageSize = 20) {
    this.fetchPage = fetchPage;
    this.pageSize = pageSize;
  }

  [Symbol.iterator]() {
    let page = 0;
    let buffer = [];
    let bufferIndex = 0;
    let exhausted = false;

    return {
      next: () => {
        if (bufferIndex < buffer.length) {
          return { value: buffer[bufferIndex++], done: false };
        }
        if (exhausted) {
          return { value: undefined, done: true };
        }
        // 同步版本需要预加载所有数据
        // 异步版本见下一节
        const result = this.fetchPage(page++, this.pageSize);
        buffer = result.items;
        bufferIndex = 0;
        exhausted = !result.hasMore;
        if (buffer.length === 0) {
          return { value: undefined, done: true };
        }
        return { value: buffer[bufferIndex++], done: false };
      }
    };
  }
}

四、异步迭代器与异步生成器:流式数据处理

ES2018引入的异步迭代协议是迭代体系最重要的扩展。它用

1
Symbol.asyncIterator

替代

1
Symbol.iterator

,用

1
next()

返回Promise,让

1
for await...of

可以优雅地处理异步数据源。

4.1 异步迭代协议


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const asyncIterable = {
  [Symbol.asyncIterator]() {
    let count = 0;
    return {
      async next() {
        // 模拟异步数据获取
        await new Promise(r => setTimeout(r, 100));
        if (count < 3) {
          return { value: count++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

(async () => {
  for await (const item of asyncIterable) {
    console.log(item); // 0, 1, 2(每个间隔100ms)
  }
})();

4.2 异步生成器函数

1
async function*

声明创建异步生成器,它在生成器的基础上支持

1
await

。这是处理流式异步数据最自然的语法:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
async function* fetchAllPages(baseUrl, pageSize = 50) {
  let offset = 0;
  let hasMore = true;

  while (hasMore) {
    const url = `${baseUrl}?offset=${offset}&limit=${pageSize}`;
    const resp = await fetch(url);
    const data = await resp.json();

    if (data.items.length === 0) {
      hasMore = false;
    } else {
      yield* data.items;  // 逐条 yield 当前页的数据
      offset += pageSize;
      hasMore = data.hasMore ?? data.items.length === pageSize;
    }
  }
}

// 使用:简洁地遍历所有分页数据
(async () => {
  for await (const item of fetchAllPages('/api/users')) {
    console.log(item.name);
  }
})();

4.3 实战:Node.js可读流转换为异步迭代器

Node.js从v10开始原生支持异步迭代器,

1
Readable

流可以直接用

1
for await...of

遍历。我们可以利用这个特性实现高效的流式处理:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { createReadStream } from 'fs';
import { createInterface } from 'readline';

// 逐行处理大文件(内存友好)
async function processLargeFile(filePath) {
  const rl = createInterface({
    input: createReadStream(filePath),
    crlfDelay: Infinity
  });

  let lineCount = 0;
  let totalBytes = 0;

  for await (const line of rl) {
    lineCount++;
    totalBytes += Buffer.byteLength(line);
    // 每处理10000行输出一次进度
    if (lineCount % 10000 === 0) {
      console.log(`已处理 ${lineCount} 行,${totalBytes} 字节`);
    }
  }

  console.log(`完成:共 ${lineCount} 行,${totalBytes} 字节`);
}

五、生成器的高级模式与工程化应用

5.1 用生成器实现有限状态机

生成器的暂停-恢复机制天然适合表示状态机。每个

1
yield

代表一个状态,

1
next(value)

的输入代表状态转换的触发事件:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* trafficLight() {
  while (true) {
    const duration = yield { light: 'green', duration: 30 };
    // 外部可以覆盖持续时间
    yield { light: 'yellow', duration: duration ?? 5 };
    yield { light: 'red', duration: 25 };
  }
}

const light = trafficLight();
console.log(light.next().value);       // { light: 'green', duration: 30 }
console.log(light.next().value);       // { light: 'yellow', duration: undefined }
console.log(light.next(3).value);      // { light: 'red', duration: 25 }
console.log(light.next().value);       // { light: 'green', duration: 30 } // 循环

5.2 用生成器实现懒加载管道(Lazy Pipeline)

函数式编程中的

1
map

1
filter

1
take

等操作如果用数组实现,每次操作都会创建新数组,对大数据集非常低效。用生成器实现懒管道,只在最终消费时才计算:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function* lazyMap(iterable, fn) {
  for (const item of iterable) {
    yield fn(item);
  }
}

function* lazyFilter(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) yield item;
  }
}

function* lazyTake(iterable, n) {
  let count = 0;
  for (const item of iterable) {
    if (count++ >= n) break;
    yield item;
  }
}

// 懒管道:从无限序列中找出前5个能被7整除的平方数
const pipeline = lazyTake(
  lazyFilter(
    lazyMap(
      (function*() { let i = 0; while (true) yield i++; })(),
      x => x * x
    ),
    x => x % 7 === 0
  ),
  5
);

console.log([...pipeline]); // [0, 49, 196, 441, 784]
// 仅计算到 28²=784 即停止,完全不会无限循环

5.3 生成器与递归:深度优先的惰性遍历

文件系统遍历是生成器加递归的典型应用场景。使用

1
yield*

委托,代码既简洁又内存高效:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { readdirSync, statSync } from 'fs';
import { join } from 'path';

function* walkDir(dir) {
  const entries = readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      yield* walkDir(fullPath);  // 递归委托
    } else {
      yield fullPath;
    }
  }
}

// 懒遍历:找到第一个 .md 文件就停止
for (const file of walkDir('/project/docs')) {
  if (file.endsWith('.md')) {
    console.log('找到文档:', file);
    break;  // 不会遍历整个目录树
  }
}

六、生成器的返回值、异常处理与清理

6.1 return()与throw()方法

生成器除了

1
next()

,还提供了

1
return(value)

1
throw(error)

两个方法,用于从外部强制结束或向生成器内部抛出异常:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* generator() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (e) {
    console.log('生成器捕获异常:', e.message);
    yield 'recovered';
  }
}

const gen = generator();
console.log(gen.next());        // { value: 1, done: false }
console.log(gen.throw(new Error('外部中断')));
// 生成器捕获异常: 外部中断 -> { value: 'recovered', done: false }
console.log(gen.next());        // { value: undefined, done: true }

// return() 会提前终止
const gen2 = generator();
console.log(gen2.next());       // { value: 1, done: false }
console.log(gen2.return(42));   // { value: 42, done: true }

6.2 finally块与资源清理

当生成器通过

1
return()

1
throw()

1
for...of

循环正常结束时,如果函数体中有

1
finally

块,它一定会被执行。这是确保资源释放的可靠机制:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* withResource() {
  const conn = openConnection();  // 获取数据库连接
  try {
    yield conn;
    // 使用者通过 next() 传入查询并获取结果
  } finally {
    // 无论生成器如何结束,连接都会被释放
    conn.close();
    console.log('连接已释放');
  }
}

// for...of 循环正常结束后,finally 块自动执行
for (const conn of withResource()) {
  // 使用连接...
  break;  // 即使 break,finally 也会执行
}

七、性能考量与常见陷阱

7.1 生成器 vs 数组:何时选择

维度 数组 生成器
内存占用 O(n),所有元素常驻内存 O(1),每次只持有一个元素
随机访问 O(1),支持索引 不支持,只能顺序访问
多次遍历 天然支持 需要重新创建生成器
创建开销 需要一次性计算所有元素 几乎零开销(惰性求值)
适用场景 数据量小、需要反复访问 大数据/无限序列、流式处理

7.2 常见陷阱

陷阱1:生成器只能遍历一次。一个生成器实例被耗尽后,再次遍历不会产出任何值。需要重新调用生成器函数创建新实例:


1
2
3
4
function* gen() { yield 1; yield 2; }
const g = gen();
console.log([...g]); // [1, 2]
console.log([...g]); // [] ← 已耗尽!

陷阱2:在异步生成器中忘记

1
await

异步生成器的

1
next()

返回Promise,但

1
for await...of

会自动处理。手动调用时别忘了

1
await


1
2
3
4
5
6
7
8
9
async function* asyncGen() {
  yield Promise.resolve(1);  // 注意:yield Promise.resolve(x) 等价于 yield x
}

const ag = asyncGen();
// 错误:ag.next() 返回 Promise,不 await 会拿到 Promise 对象
console.log(ag.next()); // Promise { { value: 1, done: false } }
// 正确
ag.next().then(result => console.log(result)); // { value: 1, done: false }

陷阱3:

1
yield<em>

不等于

1
for...of + yield

的返回值传递。

1
yield</em>

会传递委托生成器的

1
return

值,而

1
for...of

会忽略它:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* inner() {
  yield 1;
  return 'inner-result';
}

function* withYieldStar() {
  const result = yield* inner();
  console.log('yield* 返回值:', result); // 'inner-result'
}

function* withForOf() {
  for (const item of inner()) {
    yield item;
  }
  // for...of 无法获取 inner 的 return 值
}

八、实战案例:构建实时数据流处理管道

将前面所学的所有知识整合,我们来构建一个模拟实时股票数据流处理的完整管道。这个管道包含数据源(异步生成器)、转换层(懒管道操作符)和消费端:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 数据源:模拟实时股票行情
async function* stockTicker(symbols, intervalMs = 1000) {
  while (true) {
    for (const symbol of symbols) {
      await new Promise(r =&gt; setTimeout(r, intervalMs / symbols.length));
      yield {
        symbol,
        price: 100 + Math.random() * 50,
        timestamp: Date.now()
      };
    }
  }
}

// 转换:计算移动平均
async function* movingAverage(source, windowSize = 5) {
  const buffer = [];
  for await (const tick of source) {
    buffer.push(tick);
    if (buffer.length &gt; windowSize) buffer.shift();
    const avg = buffer.reduce((sum, t) =&gt; sum + t.price, 0) / buffer.length;
    yield { ...tick, movingAvg: avg.toFixed(2) };
  }
}

// 过滤:只输出波动超过阈值的数据
async function* volatilityFilter(source, threshold = 2) {
  let lastAvg = null;
  for await (const tick of source) {
    const avg = parseFloat(tick.movingAvg);
    if (lastAvg !== null &amp;&amp; Math.abs(avg - lastAvg) &gt; threshold) {
      yield { ...tick, alert: '高波动', delta: (avg - lastAvg).toFixed(2) };
    }
    lastAvg = avg;
  }
}

// 组装管道
const pipeline = volatilityFilter(
  movingAverage(
    stockTicker(['AAPL', 'GOOGL', 'TSLA'], 500),
    3
  ),
  1.5
);

// 消费(设置超时自动停止)
const controller = new AbortController();
setTimeout(() =&gt; controller.abort(), 10000);

try {
  for await (const alert of pipeline) {
    console.log(`[${new Date(alert.timestamp).toISOString()}] ` +
      `${alert.alert}: ${alert.symbol} 均价=${alert.movingAvg} 变动=${alert.delta}`);
  }
} catch (e) {
  if (e.name !== 'AbortError') throw e;
  console.log('监控已停止');
}

这个完整案例展示了异步生成器的管道式组合能力:每个处理阶段都是独立的异步生成器,可以自由插入、移除或替换,而消费端只需一个

1
for await...of

循环。这种模式在实时日志分析、IoT数据采集、WebSocket消息处理等场景中都非常实用。

总结

JavaScript的迭代器与生成器体系是一个从底层协议到高层语法的完整架构:

1
Symbol.iterator

1
Symbol.asyncIterator

定义了统一的接口契约,生成器函数

1
function<em>

1
async function</em>

提供了最简洁的实现方式,而

1
yield*

委托、双向通信、

1
return()

/

1
throw()

等高级特性则赋予了生成器远超简单遍历的表达能力。

在实际工程中,当你遇到以下场景时,请优先考虑迭代器与生成器方案:

  • 大数据集的懒加载和流式处理
  • 无限序列或按需计算的数学序列
  • 树形或图结构的深度优先遍历
  • 分页API的透明遍历
  • 实时数据流的管道式处理
  • 状态机和行为模式的声明式表达

掌握迭代器与生成器,不仅能让你写出更优雅、更高效的代码,更能打开一种全新的编程思维——从”先计算全部结果再消费”转变为”按需逐个产出”,这在资源敏感和性能关键的现代Web应用中,是一种不可替代的能力。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » JavaScript迭代器与生成器深度实战:协议原理、自定义迭代、异步生成器与工程化应用完整指南
分享到: 更多 (0)