迭代器与生成器
-
- [ES6 迭代器与生成器深度解析](#ES6 迭代器与生成器深度解析)
- 一、迭代器(Iterator)
-
- [1.1 核心概念](#1.1 核心概念)
- [1.2 最基本的迭代器使用](#1.2 最基本的迭代器使用)
- [1.3 `for...of` 循环](#1.3
for...of循环) - [1.4 展开运算符 `...` 与可迭代对象](#1.4 展开运算符
...与可迭代对象) - [1.5 自定义迭代器](#1.5 自定义迭代器)
- 二、生成器(Generator)
-
- [2.1 基本语法与行为](#2.1 基本语法与行为)
- [2.2 生成器是迭代器的工厂](#2.2 生成器是迭代器的工厂)
- [2.3 `yield*` 委托](#2.3
yield*委托) - [2.4 向生成器传入值:`next(value)`](#2.4 向生成器传入值:
next(value)) - [2.5 生成器的 `return()` 和 `throw()`](#2.5 生成器的
return()和throw())
- 三、深入应用场景
-
- [3.1 惰性求值与无限序列](#3.1 惰性求值与无限序列)
- [3.2 实现可迭代的数据结构](#3.2 实现可迭代的数据结构)
- [3.3 异步流程控制(生成器 + Promise)](#3.3 异步流程控制(生成器 + Promise))
- 四、总结对比
ES6 迭代器与生成器深度解析
在 ES6 之前,遍历一个数据集合(比如数组、对象、Map、Set)往往需要不同的方式:for、for...in、``Array.prototype.forEach` 等。ES6 引入了一套统一的迭代协议 ,让任何数据结构都可以被标准地遍历。而生成器则是创建迭代器最便捷、最强大的工具。
一、迭代器(Iterator)
1.1 核心概念
-
迭代器协议:定义了一种标准方式来产生一个有限或无限序列的值。当一个对象满足以下条件时,它就是一个迭代器:
- 它有一个
next()方法,该方法返回一个对象{ value: any, done: boolean }。 done为true表示迭代结束,value可以省略(通常为undefined)。done为false或未提供时,value是本次迭代的值。
- 它有一个
-
可迭代协议 :允许 JavaScript 对象定义或定制它们的迭代行为。一个对象如果具有
Symbol.iterator属性,且该属性是一个返回迭代器的无参函数,那么这个对象就是可迭代的(iterable)。
许多内置类型默认就是可迭代的:Array、String、Map、Set、arguments 对象、NodeList 等。
1.2 最基本的迭代器使用
javascript
// 数组本身是可迭代的,我们可以手动获取它的迭代器
const arr = ['a', 'b', 'c'];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
1.3 for...of 循环
for...of 是专门用来遍历可迭代对象的语法糖,它自动调用 Symbol.iterator 并反复调用 next(),直到 done 为 true。
javascript
const arr = ['a', 'b', 'c'];
for (const item of arr) {
console.log(item);
}
// 输出 a b c
// 字符串也是可迭代的
for (const ch of 'Hello') {
console.log(ch);
}
// H e l l o
1.4 展开运算符 ... 与可迭代对象
展开运算符内部也使用了迭代器协议,因此可以展开任何可迭代对象。
javascript
const set = new Set([1, 2, 3]);
const arr = [...set]; // [1, 2, 3]
// 甚至可以把字符串展开成字符数组
const chars = [...'abc']; // ['a', 'b', 'c']
1.5 自定义迭代器
假设我们想要一个对象,它可以产生从 start 到 end 的整数序列。我们可以手动实现 Symbol.iterator 方法。
javascript
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 };
} else {
return { done: true };
}
},
};
},
};
for (const num of range) {
console.log(num);
}
// 输出 1 2 3 4 5
// 也可以手动迭代
const it = range[Symbol.iterator]();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
// ...
注意:迭代器本身通常也可以是一个可迭代对象(即它也有
Symbol.iterator方法,返回this),这样迭代器可以直接用于for...of。上面的例子中,我们返回的迭代器对象没有实现Symbol.iterator,所以它本身不是可迭代的,但通常我们只关心range是可迭代的。
二、生成器(Generator)
手动构建一个遵循迭代器协议的对象往往需要编写很多模板代码。生成器函数 提供了一种更简洁、更强大的方式------使用 function* 定义,内部通过 yield 关键字控制执行流。
2.1 基本语法与行为
- 生成器函数:
function* name() { ... } - 调用生成器函数不会立即执行 函数体,而是返回一个生成器对象(Generator Object)。
- 生成器对象同时实现了迭代器协议 和可迭代协议。
- 每次调用生成器对象的
next()方法,函数体会从上一次暂停的位置(或开头)执行,直到遇到下一个yield或return。 yield表达式可以返回一个值,并暂停函数执行。return语句会终结生成器,done变为true,value为return的值(若省略则为undefined)。
javascript
function* simpleGenerator() {
console.log('开始执行');
yield 1;
console.log('第一次恢复');
yield 2;
console.log('第二次恢复');
return 3; // 结束,done 变为 true,value 为 3
}
const gen = simpleGenerator();
console.log(gen.next());
// 输出:开始执行
// { value: 1, done: false }
console.log(gen.next());
// 输出:第一次恢复
// { value: 2, done: false }
console.log(gen.next());
// 输出:第二次恢复
// { value: 3, done: true }
console.log(gen.next());
// { value: undefined, done: true }
2.2 生成器是迭代器的工厂
生成器函数最大的优势就是可以轻松创建自定义迭代器。我们用生成器重写之前的 range:
javascript
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const it = range(1, 5);
for (const num of it) {
console.log(num);
}
// 1 2 3 4 5
// 或者直接用 for...of 迭代生成器函数返回的对象
for (const num of range(3, 7)) {
console.log(num);
}
// 3 4 5 6 7
可以看到,代码极其简洁,不再需要手动管理 next 和状态。
2.3 yield* 委托
在生成器内部,可以使用 yield* 表达式将迭代控制权委托给另一个可迭代对象(或生成器)。这相当于把那个可迭代对象的所有值逐个 yield 出来。
javascript
function* generateNumbers() {
yield 1;
yield 2;
}
function* generateMore() {
yield 'a';
yield* generateNumbers(); // 委托
yield 'b';
yield* [10, 20]; // 委托给数组
}
for (const val of generateMore()) {
console.log(val);
}
// 输出:a, 1, 2, b, 10, 20
2.4 向生成器传入值:next(value)
next() 方法可以接收一个参数,这个参数会作为上一个 yield 表达式的返回值 (在生成器内部)。第一次调用 next() 时传参会被忽略,因为此时还没有上一个 yield。
这个特性使得生成器可以接收外部数据,实现双向通信。
javascript
function* twoWay() {
const first = yield '请输入第一个数';
const second = yield '请输入第二个数';
yield `和是 ${first + second}`;
}
const gen = twoWay();
console.log(gen.next()); // { value: '请输入第一个数', done: false }
console.log(gen.next(10)); // 10 被赋给 first,然后执行到下一个 yield
// { value: '请输入第二个数', done: false }
console.log(gen.next(20)); // 20 被赋给 second,执行 yield 和
// { value: '和是 30', done: false }
console.log(gen.next()); // { value: undefined, done: true }
注意:
next()传入的值会替换生成器内部对应的yield表达式。第一次next()调用时没有等待中的yield,所以参数被忽略。
2.5 生成器的 return() 和 throw()
generator.return(value):强制生成器结束,并返回{ value, done: true }。后续next()调用都会返回{ done: true }。generator.throw(error):在生成器当前暂停的位置抛出一个错误,如果生成器内部捕获了该错误,则可以继续执行;否则生成器终止。
javascript
function* withError() {
try {
yield 1;
yield 2;
} catch (e) {
console.log('捕获到错误:', e.message);
yield 3;
}
yield 4;
}
const gen = withError();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.throw(new Error('出错了')));
// 捕获到错误:出错了
// { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: undefined, done: true }
三、深入应用场景
3.1 惰性求值与无限序列
生成器可以表示无穷序列,因为它只在需要时计算下一个值。这避免了预先分配大量内存。
示例:无限斐波那契数列
javascript
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
// 可以一直取下去,但只占用 O(1) 内存
// 取前 10 个斐波那契数
const first10 = [];
for (let i = 0; i < 10; i++) {
first10.push(fib.next().value);
}
console.log(first10); // [5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
// 注意:上面已经消耗了前 5 个,所以从第 6 个开始
3.2 实现可迭代的数据结构
假设我们有一个自定义的链表或树,我们可以用生成器轻松定义深度优先遍历。
javascript
class TreeNode {
constructor(value, children = []) {
this.value = value;
this.children = children;
}
*[Symbol.iterator]() {
yield this.value;
for (const child of this.children) {
yield* child; // 委托给子节点的迭代器
}
}
}
// 构建树:根节点 1,子节点 2、3,其中 2 有子节点 4、5
const tree = new TreeNode(1, [
new TreeNode(2, [new TreeNode(4), new TreeNode(5)]),
new TreeNode(3),
]);
for (const val of tree) {
console.log(val);
}
// 输出:1 2 4 5 3 (前序遍历)
3.3 异步流程控制(生成器 + Promise)
在 async/await 出现之前,生成器配合 Promise 可以实现类似"同步写法"的异步代码。著名的 co 库就是基于这个思想。
原理 :生成器内部 yield 一个 Promise,外部控制函数负责 next 并把 Promise 结果传回去。
javascript
// 模拟异步任务
function fetchData(url) {
return new Promise((resolve) => {
setTimeout(() => resolve(`数据来自 ${url}`), 1000);
});
}
// 使用生成器编写异步流程
function* asyncFlow() {
const data1 = yield fetchData('https://api.example.com/user');
console.log(data1);
const data2 = yield fetchData('https://api.example.com/posts');
console.log(data2);
return '完成';
}
// 自动执行器(简化版)
function run(generatorFunc) {
const gen = generatorFunc();
function handle(result) {
if (result.done) return result.value;
// 假设 yield 出来的总是 Promise
result.value.then(
(res) => handle(gen.next(res)),
(err) => gen.throw(err)
);
}
handle(gen.next());
}
run(asyncFlow);
// 1秒后输出:数据来自 https://api.example.com/user
// 再1秒后输出:数据来自 https://api.example.com/posts
这正是 async/await 的前身,只不过现在语法更优雅。
四、总结对比
| 特性 | 手动实现迭代器 | 生成器 |
|---|---|---|
| 代码量 | 较多,需维护状态和 next 方法 | 极少,只需 function* + yield |
| 可读性 | 一般 | 非常直观,像同步函数 |
| 是否支持双向通信 | 否(除非额外实现) | 是,通过 next(value) |
| 是否支持惰性求值 | 可以,但需手动控制 | 天然支持,yield 即暂停 |
| 能否表示无限序列 | 可以,但需谨慎处理结束条件 | 可以,用 while(true) 非常自然 |
是否可被 for...of |
需要同时实现可迭代协议 | 生成器对象本身就是可迭代的 |
核心要点记忆
- 可迭代对象 有
Symbol.iterator方法,返回一个迭代器。 - 迭代器 有
next()方法,返回{ value, done }。 - 生成器函数 (
function*)调用后返回一个生成器对象,它既是迭代器也是可迭代对象。 yield让生成器可以暂停和恢复,yield*委托给其他可迭代对象。- 生成器可以用于惰性序列 、自定义遍历 以及简化异步控制流 (尽管现在更推荐直接用
async/await)。