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的整数functioncreateNumberIterator(){letcurrent=1;constmax=5;// 返回迭代器对象(符合迭代器协议)return{next(){if(current<=max){return{value:current++,done:false};}else{return{done:true};// 遍历结束,可省略value}}};}// 使用迭代器constiterator=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. 迭代器的优势:支持无限序列
普通数组无法存储无限数据(比如自然数序列),但迭代器是“按需生成”的,能轻松实现无限序列:
// 无限自然数迭代器functioncreateInfiniteIterator(){letcurrent=1;return{next(){return{value:current++,done:false};// 永远不结束}};}constinfiniteIt=createInfiniteIterator();console.log(infiniteIt.next().value);// 1console.log(infiniteIt.next().value);// 2console.log(infiniteIt.next().value);// 3// 想要多少要多少,不占额外内存三、可迭代对象:能被for...of遍历的“合格数据”
迭代器是“售货机”,但for...of不直接遍历迭代器,而是遍历“可迭代对象”——即拥有[Symbol.iterator]()方法的对象。这个方法调用后返回迭代器,相当于“售货机的说明书”,for...of会自动按说明书获取迭代器,调用next()直到done: true。
1. 内置可迭代对象
JS中数组、String、Set、Map、类型数组都是内置可迭代对象,因为它们的原型上有[Symbol.iterator]()方法:
// 数组是可迭代对象constarr=[1,2,3];constarrIt=arr[Symbol.iterator]();// 获取迭代器console.log(arrIt.next());// { value: 1, done: false }// for...of自动调用[Symbol.iterator](),遍历迭代器for(constitemofarr){console.log(item);// 1、2、3}2. 让普通对象变成可迭代对象
普通对象没有[Symbol.iterator](),所以不能用for...of。咱们给它加个“说明书”,让它变成可迭代对象:
constuser={name:"张三",hobbies:["篮球","游戏","美食"],// 实现[Symbol.iterator](),返回迭代器[Symbol.iterator](){letindex=0;consthobbies=this.hobbies;return{next(){if(index<hobbies.length){return{value:hobbies[index++],done:false};}else{return{done:true};}}};}};// 现在user是可迭代对象,能被for...of遍历for(consthobbyofuser){console.log(hobby);// 篮球、游戏、美食}// 也能使用展开语法consthobbyArr=[...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(){yield1;yield2;yield3;yield4;yield5;}// 调用生成器函数,返回生成器(迭代器)constgenerator=numberGenerator();// 生成器是迭代器,支持next()console.log(generator.next());// { value: 1, done: false }console.log(generator.next());// { value: 2, done: false }// 生成器是可迭代对象,支持for...offor(constnumofnumberGenerator()){console.log(num);// 1、2、3、4、5}更简洁的写法,用for...in或循环:
// 生成1~max的整数生成器function*rangeGenerator(start=1,end,step=1){for(leti=start;i<=end;i+=step){yieldi;}}// 遍历1~10,步长2for(constnumofrangeGenerator(1,10,2)){console.log(num);// 1、3、5、7、9}3. 生成器的暂停与恢复:智能导游的“灵活路线”
yield的核心是“暂停执行”,next()的核心是“恢复执行到下一个yield”。这个特性让生成器能实现“非连续执行”,比如斐波那契数列:
// 斐波那契数列生成器function*fibGenerator(max=Infinity){leta=0,b=1;while(b<=max){yieldb;// 暂停,返回b,下次从这里继续[a,b]=[b,a+b];}}// 遍历前5个斐波那契数constfibIt=fibGenerator();console.log(fibIt.next().value);// 1console.log(fibIt.next().value);// 1console.log(fibIt.next().value);// 2console.log(fibIt.next().value);// 3console.log(fibIt.next().value);// 5五、高级用法:生成器的“进阶技能”
1. next()传参:暂停后调整状态
next()可以传参数,这个参数会成为上一个yield的返回值,实现“暂停后给生成器传指令”:
// 带参数的生成器:根据传入值调整步长function*adjustGenerator(start=1){letstep=1;while(true){// 接收next()传入的参数,作为yield的返回值constnewStep=yieldstart;// 如果传了新步长,更新stepif(newStep)step=newStep;start+=step;}}constadjustIt=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{yield1;yield2;yield3;}catch(err){console.log("捕获异常:",err.message);yield"异常后继续执行";}}consterrIt=errorGenerator();console.log(errIt.next().value);// 1errIt.throw(newError("手动抛出异常"));// 捕获异常:手动抛出异常console.log(errIt.next().value);// 异常后继续执行3. return():提前终止生成器
return(value)让生成器立即返回{ value, done: true },后续next()都返回{ done: true }:
constgen=rangeGenerator(1,5);console.log(gen.next().value);// 1console.log(gen.return("提前终止").value);// 提前终止console.log(gen.next().done);// true六、实战场景:迭代器与生成器的“用武之地”
1. 场景1:遍历自定义数据结构(树/链表)
迭代器适合遍历复杂数据结构,比如二叉树的中序遍历:
// 二叉树节点classTreeNode{constructor(val){this.val=val;this.left=null;this.right=null;}}// 二叉树中序遍历生成器function*inorderTraversal(root){if(root){yield*inorderTraversal(root.left);// 递归遍历左子树yieldroot.val;// 返回当前节点值yield*inorderTraversal(root.right);// 递归遍历右子树}}// 构建二叉树constroot=newTreeNode(1);root.right=newTreeNode(2);root.right.left=newTreeNode(3);// 遍历二叉树for(constvalofinorderTraversal(root)){console.log(val);// 1、3、2(中序遍历结果)}2. 场景2:异步迭代(依次执行异步任务)
生成器的暂停特性适合处理异步流程,比如依次请求多个接口,前一个成功再请求下一个:
// 模拟异步请求functionfetchData(url){returnnewPromise(resolve=>{setTimeout(()=>resolve(`数据:${url}`),1000);});}// 异步生成器:依次请求接口function*asyncGenerator(urls){for(consturlofurls){constdata=yieldfetchData(url);// 暂停,等待Promise完成yielddata;// 返回数据}}// 执行异步生成器asyncfunctionrunAsyncGenerator(){consturls=["url1","url2","url3"];constgen=asyncGenerator(urls);letresult=gen.next();while(!result.done){// 如果是Promise,等待其完成constvalue=awaitresult.value;console.log(value);result=gen.next();}}runAsyncGenerator();// 每隔1秒输出一个数据3. 场景3:无限序列(按需生成,不占内存)
处理大数据量时,生成器按需生成数据,避免一次性加载所有数据导致内存溢出:
// 生成100万以内的偶数(按需生成,不占内存)function*evenGenerator(max=1000000){for(leti=2;i<=max;i+=2){yieldi;}}// 遍历前10个偶数,后面的不生成constevenIt=evenGenerator();for(leti=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通吃所有可迭代对象,不用记多种遍历语法; - 支持复杂场景:无限序列、异步迭代、自定义数据结构遍历,普通循环做不到;
- 优化性能:按需生成数据,避免一次性加载大数据导致的内存压力。
掌握它们,你就能从容应对复杂的遍历需求,写出更优雅、更高效的代码。