当前位置: 首页 > news >正文

一次性讲清楚迭代器,可迭代对象和生成器

很好,请告诉我有多少人之前都没怎么了解过这些概念的,最开始我知道它是在书上,它是这样的形式出现的:

function*(x){vary=yieldx+1;returny}

第一个反应是,这是啥,我用过吗?好像没有。哦,那就是不重要的概念,忽视之。说句实在话,直到现在我在代码中都还没用过yield这个关键字,但是,这并不影响我们实际上每天都在使用它们,比如for...of,再比如async/await。感谢前辈们,把这些功能二次封装得很好,

一、先把概念说清楚

为什么需要迭代器

先想一个问题:ES6 之前,遍历不同的数据结构,写法是不统一的。数组用索引for循环,对象用for...in,类数组(如arguments)又得转一道。后来 ES6 还新增了SetMap,数据结构越来越多,如果每种都有自己的遍历方式,代码会很乱。

迭代器的出现,就是为了给所有数据结构一个统一的遍历接口。有了它,无论底层是数组、字符串还是 Map,都能用同一套for...of来遍历。这就是这套机制存在的根本意义——统一

下面登场的三个角色,正是为了实现这个"统一"而层层搭建的。

角色一:迭代器(Iterator)——一个"按需吐值"的指针

迭代器是一个对象,它身上有一个next()方法。每调一次next(),它吐出一个结果对象:

{value:当前值,done:是否已经取完}

把它想象成一个取号机:按一下出一个号(value),并告诉你"还有没有了"(done)。取完了,done变成true

我们手动造一个迭代器,就看清它的本质了:

functionmakeIterator(arr){leti=0;return{next(){if(i<arr.length){return{value:arr[i++],done:false};}else{return{value:undefined,done:true};}}};}constit=makeIterator(['a','b']);it.next();// { value: 'a', done: false }it.next();// { value: 'b', done: false }it.next();// { value: undefined, done: true } —— 取完了

这就是迭代器的全部:一个有next()、能逐个吐值并报告是否结束的对象。这套"必须有 next()、返回 {value, done}"的约定,叫迭代器协议(Iterator Protocol)

但光有迭代器还不够——for...of怎么知道去哪儿拿这个迭代器?这就引出第二个角色。

角色二:可迭代对象(Iterable)——一个"能交出迭代器"的东西

for...of遵守另一个约定:一个对象如果想被for...of遍历,必须有一个名为Symbol.iterator的方法,这个方法返回一个迭代器。满足这个条件的对象,就叫"可迭代对象"。这套约定叫可迭代协议(Iterable Protocol)

数组、字符串、SetMap天生就有Symbol.iterator,所以它们能被for...of遍历;普通对象没有,所以不能。我们给普通对象手动加上,它立刻就能遍历了:

constobj={data:['x','y','z'],[Symbol.iterator](){// 实现这个方法,返回一个迭代器leti=0;return{next:()=>{returni<this.data.length?{value:this.data[i++],done:false}:{value:undefined,done:true};}};}};for(constvofobj)console.log(v);// x y z —— 现在能遍历了!

两个角色的分工很清楚:

角色关键方法职责
可迭代对象(Iterable)Symbol.iterator()交出一个迭代器
迭代器(Iterator)next()逐个吐值,报告是否结束

for...of的完整工作流程就是:先调对象的Symbol.iterator()拿到迭代器,然后不停调next()取值,直到done: true停止。

到这里你应该感觉到了:手写迭代器很啰嗦——又要维护索引、又要判断done、又要拼{value, done}。有没有更省事的办法?有,这就是第三个角色。

角色三:生成器(Generator)——自动造迭代器的快捷方式

生成器的意义,就是把手写迭代器那套啰嗦的活儿自动化。你只管用yield把值一个个"吐"出来,生成器自动帮你生成符合迭代器协议的next(){value, done}

把前面手写的迭代器用生成器重写:

function*gen(arr){// function* 表示这是个生成器for(constxofarr){yieldx;// 每个 yield 自动对应一次 next() 的产出}}constit=gen(['a','b']);it.next();// { value: 'a', done: false } —— 格式和手写的完全一样!it.next();// { value: 'b', done: false }it.next();// { value: undefined, done: true }

对比一下,生成器替你做了这些事:

手写迭代器生成器
自己维护索引i自动记住执行到哪了(靠 yield 暂停)
自己判断done函数跑完自动done: true
自己拼{ value, done }yield x自动产出这个结构
一堆样板代码几行搞定

而且有个关键特性:生成器调用后返回的对象,本身既是迭代器、又是可迭代对象(它的Symbol.iterator默认返回它自己)。所以它能直接next(),也能直接for...of

function*gen(){yield1;yield2;yield3;}for(constxofgen())console.log(x);// 1 2 3 —— 直接能遍历!

这就是生成器被称为"造迭代器的快捷方式"的原因:它一举满足了两个协议,省掉了所有样板代码。

核心机制:yield 是一扇"旋转门"

生成器还有一个比"自动造迭代器"更深的能力——yield双向传递数据。这是它最容易被忽略、也最强大的地方。

方向一:yield 把值吐出去。yield 10让函数暂停,并把10通过next()返回对象的value交给外面。

方向二:外面把值塞回来。这一步最反直觉:yield 10这个表达式的返回值不是 10,而是外面下一次调用next(X)时传进来的那个 X

function*gen(){consta=yield10;// a 的值,由外面下次 next() 传入决定console.log('a =',a);}constg=gen();g.next();// 跑到 yield 10 暂停,吐出 10g.next(99);// 把 99 塞回给 yield,于是 a = 99,打印 "a = 99"

把两个方向合起来,yield就像一扇旋转门:暂停时把一个值递出去,恢复时从外面接一个值进来。

const a = yield X这行,X是吐给外面的(出现在next()返回的 value 里),而a拿到的是外面下次next(值)塞回来的值。一出一进,发生在同一个yield上。

这扇旋转门是生成器所有高级应用的基础。它最著名的应用,就是async/await——下面这一节我们就亲手把它造出来,你会彻底明白yield的双向传值到底有什么用。


二、旋转门的杀手级应用:co 函数与 async/await 的由来

开头我提到,我们其实每天都在用生成器,比如async/awaitasync/await在底层就是"生成器 + 一个自动驱动器",而这个自动驱动器,历史上有个著名的实现叫co 函数。理解了 co,你就理解了async/await的本质。

先看一个理想场景

假设我们yield出去的不是普通值,而是一个Promise

function*gen(){constuser=yieldfetchUser();// 吐出一个 Promiseconstposts=yieldfetchPosts(user);// 用上一步的结果,再吐出一个 Promiseconsole.log(posts);}

设想有个"外面的人"这样配合这个生成器:

  1. 生成器yield出一个 Promise,暂停;
  2. 外面接住这个 Promise,等它 resolve
  3. 等到了,把结果通过next(结果)塞回去——于是user拿到了真实数据(这正是上一节"旋转门"的方向二);
  4. 生成器从暂停处继续,跑到下一个yield,吐出第二个 Promise,再暂停;
  5. 外面再等、再塞回去……循环往复,直到生成器结束。

如果真有这么个"外面的人"自动做第 2、3 步,这段生成器读起来就和同步代码一模一样const user = yield fetchUser()看上去就是"等到用户数据、赋值给 user",但中间的等待是非阻塞的。

那个"外面自动帮你等、帮你塞回结果"的人,就是co 函数

亲手写一个 co 函数

co 要做的,就是把"等 Promise → 塞回结果 → 继续 → 再等"这个循环自动化。核心就十几行:

functionco(generator){constg=generator();// 调用生成器函数,拿到生成器对象(遥控器)functionstep(value){constresult=g.next(value);// 把上一步的结果塞回去,推进到下一个 yield// result = { value: 吐出来的 Promise, done: 是否结束 }if(result.done)return;// 生成器跑完了,收工// result.value 是个 Promise,等它 resolve,// 再把结果通过 step 塞回去,驱动下一步result.value.then(res=>step(res));}step();// 第一次启动(首次 next 不需要传值)}

用语言把step这个循环读一遍,就是"旋转门"的故事在自动转:

  • g.next(value):把上次等到的结果塞回去,生成器从暂停处继续,跑到下一个yield吐出新 Promise;
  • if (result.done) return:如果生成器已跑完,停止;
  • result.value.then(res => step(res)):否则等这个 Promise 好了,拿到结果再调一次step塞回去,进入下一轮。

配合使用:

co(function*(){constuser=yieldfetchUser();constposts=yieldfetchPosts(user);console.log(posts);});

这就是全部魔法。co 不停地"推进生成器 → 拿到一个 Promise → 等它 → 把结果喂回去 → 再推进",直到生成器结束。没有任何黑魔法,全是上一节那扇旋转门的机械重复。

async/await 就是内置的 co

现在把上面那段和async/await并排放:

// 生成器 + co // async/awaitco(function*(){asyncfunctionload(){constuser=yieldfetchUser();constuser=awaitfetchUser();constposts=yieldfetchPosts(user);constposts=awaitfetchPosts(user);console.log(posts);console.log(posts);});}load();

几乎一模一样,差别只有三处:

生成器 + coasync/await
function*async function
yieldawait
需要手动包一个co不需要,引擎内置了

核心结论:async/await本质上就是"生成器语法 + 一个内置的自动执行器"。await干的事和"yield 出一个 Promise"完全相同——暂停函数、等 Promise resolve、把结果塞回来继续。你不用写 co,是因为 JS 引擎在背后替你做了。

这就回答了开头那个困惑:你没直接用过yield,却天天在用async/await——因为后者就是前者被引擎封装好的样子。前辈们把这套机制二次封装得太好,好到你感觉不到生成器的存在。


三、用面试题把概念吃透

下面是几道真实的前端面试题,覆盖迭代器/生成器的高频考点。每题先自己推一遍,再看答案和解析

题目 1:执行时机
function*foo(){console.log('A');yield;console.log('B');yield;console.log('C');}constg=foo();console.log('start');g.next();g.next();

先想想:输出顺序是什么?const g = foo()这行会打印 A 吗?

答案与解析

答案:start → A → B

逐步推演:

  1. const g = foo()——不打印任何东西。调用生成器函数不执行函数体,只返回一个处于暂停状态的生成器对象。这是最高频的考点。
  2. console.log('start')—— 打印start
  3. 第一次g.next()—— 函数开始执行,打印A,遇到第一个yield暂停。
  4. 第二次g.next()—— 从暂停处恢复,打印B,遇到第二个yield暂停。
  5. 代码结束,C没机会打印(需要第三次 next)。

关键认知:调用生成器函数 ≠ 执行它。它返回一个"遥控器",函数体一行都没跑,必须靠next()一步步驱动;每次next()跑到下一个yield为止。

题目 2:next 传参(双向传值)
function*gen(){constx=yield'first';consty=yieldx+1;returnx+y;}constg=gen();console.log(g.next());// ?console.log(g.next(10));// ?console.log(g.next(20));// ?

先想想:三次 next 各返回什么?x 和 y 分别是多少?

答案与解析

答案:{value:'first',done:false}{value:11,done:false}{value:30,done:true}

逐步推演(盯住"旋转门"那条认知):

  1. g.next()—— 函数跑到yield 'first'暂停,吐出'first'。返回{value:'first', done:false}。注意:第一次 next 的传参没有意义,因为此时还没有哪个 yield 在等待接收。
  2. g.next(10)—— 把10塞回给上一个yield 'first',于是x = 10。继续执行到yield x + 1yield 11,吐出11,暂停。返回{value:11, done:false}
  3. g.next(20)—— 把20塞回给yield x+1,于是y = 20。执行return x + yreturn 30,函数结束。返回{value:30, done:true}

关键认知:yield表达式的值,来自下一次next()的参数,不是 yield 后面那个值。这正是生成器能实现 async/await 的根基——把 Promise 的结果通过 next 塞回去。

题目 3:生成器只能迭代一次
function*gen(){yield1;yield2;yield3;}constg=gen();constarr1=[...g];constarr2=[...g];console.log(arr1);// ?console.log(arr2);// ?

先想想:arr1 和 arr2 分别是什么?

答案与解析

答案:arr1 是[1, 2, 3],arr2 是[](空数组)

解析:生成器对象的Symbol.iterator返回的是它自己,而不是一个全新的迭代器。所以它是个"一次性"的可迭代对象——第一次[...g]已经把它迭代到done: true耗尽了,第二次再迭代,它直接返回done: true,啥也取不到。

这区别于数组:数组每次调用Symbol.iterator都返回一个全新的迭代器,所以能反复遍历。

关键认知:生成器对象只能迭代一次,迭代完就耗尽了。如果需要反复遍历,要么每次重新调用生成器函数拿新对象(gen()),要么把结果先存进数组。这是实际开发里容易踩的坑。

题目 4:手写一个可迭代对象
// 让下面这个 range 对象能被 for...of 遍历,输出 1 2 3 4 5constrange={from:1,to:5};for(constnofrange)console.log(n);// 目标:1 2 3 4 5

先想想:怎么给 range 加上可迭代能力?至少有两种写法。

答案与解析

答案:实现Symbol.iterator方法。有两种写法,体现了"手写迭代器"和"用生成器"的区别。

写法一,手写迭代器(啰嗦版):

constrange={from:1,to:5,[Symbol.iterator](){letcurrent=this.from;constlast=this.to;return{next(){returncurrent<=last?{value:current++,done:false}:{value:undefined,done:true};}};}};

写法二,用生成器(简洁版)——因为生成器自动满足迭代器协议:

constrange={from:1,to:5,*[Symbol.iterator](){// 注意这个 * ,把方法定义成生成器for(leti=this.from;i<=this.to;i++){yieldi;}}};

两种写法效果一样,但写法二少了维护current、判断done、拼{value, done}的所有样板代码。

让对象可迭代 = 给它实现Symbol.iterator;用生成器实现是最省事的方式。这道题几乎是"自定义可迭代对象"的标准面试题。

题目 5:yield* 委托
function*inner(){yield'a';yield'b';}function*outer(){yield1;yield*inner();// 注意这个星号yield2;}console.log([...outer()]);// ?

先想想:yield*yield有什么区别?输出是什么?

答案与解析

答案:[1, 'a', 'b', 2]

解析:yield*(带星号)是委托——它把迭代的控制权交给另一个可迭代对象(这里是inner()),把对方产出的值逐个接力吐出来,而不是把整个对象当成一个值吐出去。

对比一下:

  • yield inner()—— 会把inner()这个生成器对象整体作为一个值吐出,结果是[1, 生成器对象, 2]
  • yield* inner()—— 把inner()产出的'a''b'一个个接力吐出,结果是[1, 'a', 'b', 2]

yield*用于把一个生成器/可迭代对象的值"摊平"接力出来,常用于生成器之间的组合复用。一字之差(星号),行为完全不同。

题目 6:用生成器表达无限序列(惰性求值)
function*naturalNumbers(){letn=1;while(true){// 无限循环yieldn++;}}constnums=naturalNumbers();

先想想:这个while(true)会不会把页面卡死?为什么?怎么从里面取前 3 个数?

答案与解析

答案:不会卡死。取值:nums.next().value连续调三次得到 1、2、3。

解析:普通函数里写while(true)会同步死循环、卡死主线程(这是阻塞)。但生成器不会——因为它是惰性求值的:yield n++每次只产出一个值就暂停,主动权在外面。你调一次next()它才前进一步,不调它就一直停着。

nums.next().value;// 1nums.next().value;// 2nums.next().value;// 3 —— 要几个给几个,永远不会一次性算完

生成器的"可暂停"让它能表示无限序列而不撑爆内存。普通函数必须一次性算完所有值,生成器则是"用一个才算一个"。这是生成器区别于普通函数的独特价值,也是它在处理大数据流、分页加载等场景的用武之地。

http://www.jsqmd.com/news/1101671/

相关文章:

  • AI编程工具“智能幻觉”实录:我们故意注入137处边界漏洞,仅2款工具识别率超89%(附对抗测试用例库)
  • 现在不掌握AI编程协同工作流,半年后将被淘汰:一线大厂内部推行的「人机双审」开发SOP首次公开
  • Go 错误处理机制详解:新手从 err != nil 到 errors.Is/As
  • 亲测!性价比高的口腔清洗诊所实践分享
  • XZ2616 输入电压:4.5V-16V 输出电压:ADJ 同步降压恒压芯片
  • 如何用电子课本下载工具解决教学资源离线难题:教师必备指南
  • Duplicity+GPG加密备份到DigitalOcean Spaces实战指南
  • [特殊字符] 深度解析:Agent 的原理与构建模式 —— 从零打造 Claude Code
  • 并行物理信息神经网络PINNs在NLS–MB 方程的孤子演化预测实例 【 torch求解】(Python代码实现)
  • SVG-Edit:浏览器中的专业矢量图形编辑器完整指南
  • 为什么92%的开发者用错Claude Code?3个致命误区正在拖垮你的开发交付周期
  • 零SQL基础实现数据库连接与查询:WorkBuddy无代码取数实战指南
  • AI 电动无人机智能动力 MOSFET 完整选型方案
  • 晒眼皮并不能防近视!帮孩子护眼,做好这五条才是关键!
  • 3分钟快速上手!tchMaterial-parser让您高效获取智慧教育平台电子课本
  • 魔珐星云 SDK 实战:给 Agent 一副可交互的身体
  • AI Git Helper:一键生成智能Commit
  • Java后端转AI应用开发:收藏这份90天学习路线,拒绝被算法论文吓住!
  • Temu 海量 SKU 合规攻略,用凌风工具箱批量上传合规信息降低失误
  • SQL Server 2022 Docker 容器化部署配置规范与注意事项
  • 佛山家具企业亲测:如何通过创新提升销量?
  • 微信小程序接口签名逆向实战:从抓包到算法复现
  • 3步实现Windows电脑直接运行安卓应用:免费高效的跨平台解决方案
  • 警惕“AI幻觉陷阱”:5类高危场景中AI生成代码的静态扫描漏洞率高达43%,附自动化检测SOP清单
  • 从写注释到写架构:AI工具如何重构开发生命周期?——基于137个企业项目的真实演进路径(含ROI测算模型)
  • AI代码审查工具避坑指南(血泪教训版):3个导致线上事故的误报案例,以及精准率超94.2%的调优配置
  • 毕设分享 yolov8叶片病害检测系统(源码+论文)
  • 2026 深度解读:存量竞争下新媒体代运营的核心竞争力
  • 计算机毕业设计之基于逻辑回归的天猫用户忠诚度分析与预测正文
  • Claude Code + Cursor + 星云 Skill:给 Agent 一副可交互的身体