JavaScript学习笔记:15.迭代器与生成器
上一篇用类型数组搞定了二进制数据的“高效存储”,这一篇咱们解锁JS遍历的“终极形态”——迭代器(Iterators)与生成器(Generators)。你肯定用过for循环遍历数组,用for...of遍历Set,但有没有想过:为什么数组能直接用for...of,普通对象却不行?为什么有些遍历能“暂停”,比如异步请求依次执行?这些问题的答案,都藏在迭代器和生成器里。
简单说,迭代器是“遍历说明书”——告诉程序如何一步步取出数据;生成器是“智能导游”——不仅能按说明书带路,还能随时暂停、接收指令调整路线。今天咱们就用“旅游”的生活化比喻,把这对“遍历搭档”的原理、用法和实战价值彻底讲透,让你写出更灵活、更优雅的遍历代码。
一、先破案:为什么需要迭代器?普通循环不够用吗?
普通循环(for、while)就像“自己开车逛景区”——路线得自己规划,停车点得自己记,遇到复杂数据结构(比如树、链表)就手忙脚乱。咱们先看普通循环的三大痛点:
- 遍历逻辑不统一:遍历数组要记索引(
i从0到length-1),遍历Set用forEach,遍历Map要forEach或entries(),每种数据结构一套逻辑,记起来麻烦; - 无法暂停与恢复:循环一旦启动就必须跑完,想在遍历中等待异步任务(比如遍历请求列表,前一个请求完成再发下一个)根本做不到;
- 普通对象不能直接遍历:
for...of能遍历数组/Set/Map,却不能直接遍历普通对象,得先转成Object.keys(obj),多此一举。
而迭代器和生成器的出现,就是为了解决这些问题:
- 统一遍历逻辑:不管是数组、自定义数据结构,还是树/链表,都用
for...of遍历,不用记不同语法; - 支持暂停恢复:遍历过程中能暂停,等待异步任务完成再继续,完美适配异步场景;
- 让任意对象可遍历:给普通对象加个“遍历说明书”,就能直接用
for...of遍历。
二、迭代器:遍历的“基础协议”——像台“自动售货机”
迭代器的核心是“迭代器协议”——一个对象只要有next()方法,且返回{ value: 下一个值, done: 是否结束 },它就是迭代器。就像自动售货机:投币(调用next())→ 出商品(value)→ 售罄(done: true)。
1. 迭代器的核心规则
- 必须有
next()方法,无参数或一个参数; next()返回对象必须包含done(布尔值),可选包含value;- 迭代器是“一次性消耗”的:遍历到
done: true后,再调用next(),永远返回{ done: true }。
2. 手动实现迭代器:体验“售货机”的工作原理
咱们自定义一个“1~5的整数迭代器”,手动实现迭代器协议,理解底层逻辑:
// 自定义迭代器:生成1~5的整数
function createNumberIterator() {
let current = 1;
const max = 5;
// 返回迭代器对象(符合迭代器协议)
return {
next() {
if (current <= max) {
return { value: current++, done: false };
} else {
return { done: true }; // 遍历结束,可省略value
}
}
};
}
// 使用迭代器
const iterator = createNumberIterator();
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: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { done: true }
console.log(iterator.next()); // { done: true }(已消耗,永远返回结束)
这个例子能直观看到:迭代器通过闭包维护current状态,每次next()推进状态,直到done: true。
3. 迭代器的优势:支持无限序列
普通数组无法存储无限数据(比如自然数序列),但迭代器是“按需生成”的,能轻松实现无限序列:
// 无限自然数迭代器
function createInfiniteIterator() {
let current = 1;
return {
next() {
return { value: current++, done: false }; // 永远不结束
}
};
}
const infiniteIt = createInfiniteIterator();
console.log(infiniteIt.next().value); // 1
console.log(infiniteIt.next().value); // 2
console.log(infiniteIt.next().value); // 3
// 想要多少要多少,不占额外内存
三、可迭代对象:能被for...of遍历的“合格数据”
迭代器是“售货机”,但for...of不直接遍历迭代器,而是遍历“可迭代对象”——即拥有[Symbol.iterator]()方法的对象。这个方法调用后返回迭代器,相当于“售货机的说明书”,for...of会自动按说明书获取迭代器,调用next()直到done: true。
1. 内置可迭代对象
JS中数组、String、Set、Map、类型数组都是内置可迭代对象,因为它们的原型上有[Symbol.iterator]()方法:
// 数组是可迭代对象
const arr = [1, 2, 3];
const arrIt = arr[Symbol.iterator](); // 获取迭代器
console.log(arrIt.next()); // { value: 1, done: false }
// for...of自动调用[Symbol.iterator](),遍历迭代器
for (const item of arr) {
console.log(item); // 1、2、3
}
2. 让普通对象变成可迭代对象
普通对象没有[Symbol.iterator](),所以不能用for...of。咱们给它加个“说明书”,让它变成可迭代对象:
const user = {
name: "张三",
hobbies: ["篮球", "游戏", "美食"],
// 实现[Symbol.iterator](),返回迭代器
[Symbol.iterator]() {
let index = 0;
const hobbies = this.hobbies;
return {
next() {
if (index < hobbies.length) {
return { value: hobbies[index++], done: false };
} else {
return { done: true };
}
}
};
}
};
// 现在user是可迭代对象,能被for...of遍历
for (const hobby of user) {
console.log(hobby); // 篮球、游戏、美食
}
// 也能使用展开语法
const hobbyArr = [...user];
console.log(hobbyArr); // ["篮球", "游戏", "美食"]
3. 关键区别:迭代器 vs 可迭代对象
| 特性 | 迭代器 | 可迭代对象 |
|---|---|---|
| 核心标识 | 有next()方法 | 有[Symbol.iterator]()方法 |
| 作用 | 提供遍历的具体逻辑 | 提供迭代器的“创建说明书” |
能否被for...of遍历 | 不能 | 能 |
| 例子 | createNumberIterator()返回值 | 数组、Set、自定义user对象 |
四、生成器:简化迭代器的“智能导游”
手动实现迭代器需要维护状态(比如current、index),麻烦且容易出错。生成器(Generator)是JS提供的“捷径”——用function*定义的函数,调用后返回生成器(同时是迭代器+可迭代对象),yield关键字实现暂停,自动维护状态,让迭代器创建变得超简单。
1. 生成器的核心语法
- 函数定义:
function* 函数名()(注意*); - 暂停标识:
yield value(返回value给next(),暂停执行); - 调用生成器函数:返回生成器对象(不是执行函数体);
- 生成器是迭代器:有
next()方法,也有[Symbol.iterator]()(返回自身)。
2. 用生成器简化迭代器:一行顶十行
之前的“1~5整数迭代器”,用生成器实现只要3行:
// 生成器函数:生成1~5的整数
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
// 调用生成器函数,返回生成器(迭代器)
const generator = numberGenerator();
// 生成器是迭代器,支持next()
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
// 生成器是可迭代对象,支持for...of
for (const num of numberGenerator()) {
console.log(num); // 1、2、3、4、5
}
更简洁的写法,用for...in或循环:
// 生成1~max的整数生成器
function* rangeGenerator(start = 1, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
// 遍历1~10,步长2
for (const num of rangeGenerator(1, 10, 2)) {
console.log(num); // 1、3、5、7、9
}
3. 生成器的暂停与恢复:智能导游的“灵活路线”
yield的核心是“暂停执行”,next()的核心是“恢复执行到下一个yield”。这个特性让生成器能实现“非连续执行”,比如斐波那契数列:
// 斐波那契数列生成器
function* fibGenerator(max = Infinity) {
let a = 0, b = 1;
while (b <= max) {
yield b; // 暂停,返回b,下次从这里继续
[a, b] = [b, a + b];
}
}
// 遍历前5个斐波那契数
const fibIt = fibGenerator();
console.log(fibIt.next().value); // 1
console.log(fibIt.next().value); // 1
console.log(fibIt.next().value); // 2
console.log(fibIt.next().value); // 3
console.log(fibIt.next().value); // 5
五、高级用法:生成器的“进阶技能”
1. next()传参:暂停后调整状态
next()可以传参数,这个参数会成为上一个yield的返回值,实现“暂停后给生成器传指令”:
// 带参数的生成器:根据传入值调整步长
function* adjustGenerator(start = 1) {
let step = 1;
while (true) {
// 接收next()传入的参数,作为yield的返回值
const newStep = yield start;
// 如果传了新步长,更新step
if (newStep) step = newStep;
start += step;
}
}
const adjustIt = adjustGenerator(1);
console.log(adjustIt.next().value); // 1(第一次传参无效)
console.log(adjustIt.next(2).value); // 3(步长改为2:1+2)
console.log(adjustIt.next(3).value); // 6(步长改为3:3+3)
console.log(adjustIt.next(1).value); // 7(步长改为1:6+1)
2. throw():暂停时抛出异常
throw()方法给生成器抛出异常,异常会在当前暂停的yield处抛出,可在生成器内部捕获:
function* errorGenerator() {
try {
yield 1;
yield 2;
yield 3;
} catch (err) {
console.log("捕获异常:", err.message);
yield "异常后继续执行";
}
}
const errIt = errorGenerator();
console.log(errIt.next().value); // 1
errIt.throw(new Error("手动抛出异常")); // 捕获异常:手动抛出异常
console.log(errIt.next().value); // 异常后继续执行
3. return():提前终止生成器
return(value)让生成器立即返回{ value, done: true },后续next()都返回{ done: true }:
const gen = rangeGenerator(1, 5);
console.log(gen.next().value); // 1
console.log(gen.return("提前终止").value); // 提前终止
console.log(gen.next().done); // true
六、实战场景:迭代器与生成器的“用武之地”
1. 场景1:遍历自定义数据结构(树/链表)
迭代器适合遍历复杂数据结构,比如二叉树的中序遍历:
// 二叉树节点
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
// 二叉树中序遍历生成器
function* inorderTraversal(root) {
if (root) {
yield* inorderTraversal(root.left); // 递归遍历左子树
yield root.val; // 返回当前节点值
yield* inorderTraversal(root.right); // 递归遍历右子树
}
}
// 构建二叉树
const root = new TreeNode(1);
root.right = new TreeNode(2);
root.right.left = new TreeNode(3);
// 遍历二叉树
for (const val of inorderTraversal(root)) {
console.log(val); // 1、3、2(中序遍历结果)
}
2. 场景2:异步迭代(依次执行异步任务)
生成器的暂停特性适合处理异步流程,比如依次请求多个接口,前一个成功再请求下一个:
// 模拟异步请求
function fetchData(url) {
return new Promise(resolve => {
setTimeout(() => resolve(`数据:${url}`), 1000);
});
}
// 异步生成器:依次请求接口
function* asyncGenerator(urls) {
for (const url of urls) {
const data = yield fetchData(url); // 暂停,等待Promise完成
yield data; // 返回数据
}
}
// 执行异步生成器
async function runAsyncGenerator() {
const urls = ["url1", "url2", "url3"];
const gen = asyncGenerator(urls);
let result = gen.next();
while (!result.done) {
// 如果是Promise,等待其完成
const value = await result.value;
console.log(value);
result = gen.next();
}
}
runAsyncGenerator(); // 每隔1秒输出一个数据
3. 场景3:无限序列(按需生成,不占内存)
处理大数据量时,生成器按需生成数据,避免一次性加载所有数据导致内存溢出:
// 生成100万以内的偶数(按需生成,不占内存)
function* evenGenerator(max = 1000000) {
for (let i = 2; i <= max; i += 2) {
yield i;
}
}
// 遍历前10个偶数,后面的不生成
const evenIt = evenGenerator();
for (let i = 0; i < 10; i++) {
console.log(evenIt.next().value); // 2、4、6...20
}
七、避坑指南:这些坑千万别踩
- 迭代器是一次性的:遍历到
done: true后,再调用next()也不会重置,需重新创建迭代器; - 生成器不能重复迭代:一个生成器对象只能遍历一次,再次遍历需重新调用生成器函数;
- next()第一次传参无效:第一次调用
next()时,生成器还没执行到任何yield,传参不会被接收; - 普通对象不是可迭代对象:别直接用
for...of遍历普通对象,需手动实现[Symbol.iterator](); - yield只能在生成器函数内使用:普通函数不能用
yield,会报错。
八、总结:迭代器与生成器的核心价值
迭代器定义了“统一的遍历协议”,让不同数据结构的遍历逻辑标准化;生成器简化了迭代器的创建,提供了“暂停/恢复”的强大特性,两者结合让JS的遍历能力从“手动开车”升级为“智能导游”。
核心价值总结:
- 统一遍历逻辑:
for...of通吃所有可迭代对象,不用记多种遍历语法; - 支持复杂场景:无限序列、异步迭代、自定义数据结构遍历,普通循环做不到;
- 优化性能:按需生成数据,避免一次性加载大数据导致的内存压力。
掌握它们,你就能从容应对复杂的遍历需求,写出更优雅、更高效的代码。
1106

被折叠的 条评论
为什么被折叠?



