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 => 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 > windowSize) buffer.shift();
const avg = buffer.reduce((sum, t) => 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 && Math.abs(avg - lastAvg) > 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(() => 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应用中,是一种不可替代的能力。
汤不热吧