遍历数据是编程中最常见的操作之一。ES6 引入的迭代器与生成器,为 JavaScript 提供了一套统一、可扩展的数据遍历机制,让自定义数据结构也能轻松融入 for...of、展开运算符等语法,同时也为异步流程控制带来了新的可能性。
11.4.1 迭代协议:统一的遍历约定
迭代协议是迭代器与生成器的底层基础,它定义了“怎样才算是可遍历的”以及“怎样进行遍历”。
可迭代对象(Iterable)
一个对象要想成为可迭代对象,必须实现 Symbol.iterator 方法。该方法返回一个迭代器对象。
本质上,任何实现了 [Symbol.iterator]() { ... } 结构的对象,都可以被 for...of 循环消费。数组、字符串、Map、Set 等内置数据结构都已经遵循了迭代协议,因此我们才能直接用 for...of 遍历它们。
const arr = [1, 2, 3];
// arr 内置了 Symbol.iterator 方法
for (const item of arr) {
console.log(item); // 1, 2, 3
}
迭代器对象(Iterator)
迭代器对象必须提供一个 next() 方法,该方法返回形如 { value: any, done: boolean } 的结果:
value表示当前迭代的值done为true表示遍历已经结束
调用一次 next(),迭代器向前“推进”一步。这种惰性求值的方式意味着数据不需要一次性全部生成,可以按需计算,节省内存。
// 手动调用迭代器
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
11.4.2 自定义可迭代对象
掌握了迭代协议,我们可以让任何对象都支持 for...of。比如实现一个简单的范围迭代器:
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
for...of、展开运算符 [...obj]、Array.from()、解构赋值等语法,都依赖迭代协议。当你将对象实现了 Symbol.iterator,这些工具将自动生效,无需额外改造。
11.4.3 Generator:迭代器的语法糖
手动维护迭代器中的状态(如上例中的 current)有时会比较繁琐,特别是当遍历逻辑复杂时。Generator 函数正是为了解决这一痛点而生的——它可以让你用更简洁的代码来生成一系列值,并且隐式地遵循了迭代协议。
定义与执行
Generator 函数使用 function* 声明,内部通过 yield 关键字“暂停”执行并产出一个值。
function* countUpTo(max) {
for (let i = 1; i <= max; i++) {
yield i;
}
}
const gen = countUpTo(3);
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
当调用 countUpTo(3) 时,并不会立即执行函数体,而是返回一个迭代器对象。每次调用该迭代器的 next(),Generator 函数体才会执行到下一个 yield 语句,然后暂停,并将 yield 后面的值作为 next() 的返回值。这一特性称为协程式的执行——函数可以在中途暂停,然后再被唤醒继续执行,同时保留其局部状态。
yield 的双向通信yield 不仅能向外产出值,还能从外部接收值。调用 next() 时可以传入参数,这个参数会成为上一个 yield 表达式的返回值。
function* conversation() {
const name = yield "What is your name?";
yield `Hello, ${name}!`;
}
const talk = conversation();
console.log(talk.next().value); // "What is your name?"
console.log(talk.next("Alice").value); // "Hello, Alice!"
这一特性让 Generator 能够实现比普通迭代器更复杂的控制流,比如状态机、任务调度等。
11.4.4 异步迭代与 for...of
Generator 本身是同步的,但在实际开发中,我们经常需要异步地拿到数据(比如分批读取数据库)。ES2018 引入了异步迭代器(Symbol.asyncIterator)和 for await...of 循环,来处理异步数据流。而 Generator 还有异步版本:async function*,结合 yield 可以产出 Promise,配合 for await...of 实现极大简化。
// 模拟分页获取数据的异步生成器
async function* fetchPages(url, totalPages) {
for (let i = 1; i <= totalPages; i++) {
const response = await fetch(`${url}?page=${i}`);
const data = await response.json();
yield data;
}
}
(async () => {
for await (const pageData of fetchPages('/api/posts', 3)) {
console.log(pageData); // 依次输出每页数据
}
})();
for await...of 会自动等待每个 Promise 解析完成,再继续下一次迭代。这种模式在处理流式数据、大数据集或网络分页时非常实用。
11.4.5 实际应用场景
1. 自定义数据结构的遍历
当你的类需要一种自然的遍历接口时,实现 Generator 版本的 Symbol.iterator 能让代码简洁优雅。
class Stack {
constructor() {
this.items = [];
}
push(...vals) {
this.items.push(...vals);
}
*[Symbol.iterator]() {
for (let i = this.items.length - 1; i >= 0; i--) {
yield this.items[i];
}
}
}
2. 惰性求值与无限序列
Generator 只在需要时才计算下一个值,可以表示无限序列而不会撑爆内存。
function* fibonacci() {
let [a, b] = [0, 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
3. 异步流程控制
在 async/await 普及之前,Generator 曾是处理异步的核心手段(例如 Redux-Saga 就大量使用了 Generator)。即使今天,使用 Generator 管理多步骤任务、实现取消和暂停,仍然是某些库的底层选择。
4. 实现自定义控件遍历
业务中需要按特定顺序遍历多层嵌套数据时,Generator 能把复杂的递归逻辑拆解为平铺的 yield 序列,代码可读性显著提升。
11.4.6 小结
迭代器与生成器并不是一门新语言,而是对“遍历”这一行为的标准化。迭代协议让我们得以用统一的方式消费数据结构,而 Generator 则让编写迭代器变得轻松且表达能力更强。理解这两个概念,你不仅能写出更干净的代码,还能更深入地理解 for...of、展开运算符以及现代前端框架中状态流转机制的底层原理。在需要处理惰性计算、异步流控、复杂遍历等场景时,它们将是最得力的工具。