深入解析JavaScript Generator 生成器的概念及应用场景

简介: 本文讲解了JS生成器的概念和应用场景。生成器是一个可以暂停和恢复执行的函数。利用生成器我们可以很方便地实现自定义的可迭代对象、状态机、惰性计算等,并且还能用它来简化我们的异步操作代码。

生成器(Generator)是 ES6 中引入的语言特性,其本质是一个可以暂停和恢复执行的函数。利用生成器我们可以很方便地实现自定义的可迭代对象(Iterable)、状态机(state machine)、惰性计算(lazy evaluation)等,并且还能用它来简化我们的异步操作代码,这些在文章中都会有具体的例子介绍。

生成器是一类特殊的迭代器(Iterator)。所以要了解生成器,首先我们要学习迭代器的概念。

迭代器(Iterator)

简单地说,迭代器就是实现了next()方法的一类特殊的对象。这个next()方法的返回值是一个对象,包含了valuedone两个属性。例如:

someIterator.next() // { value: 'something', done: false } someIterator.next() // { value: 'anotherThing', done: false } someIterator.next() // { value: undefined, done: true } 

代码中的someIterator就是一个实现了迭代器模式的对象,其中包含了somethinganotherThing两个值。我们通过调用next()方法,每次从其中取出一个值。

next()方法的返回值中:value的值是从对象中取出的值,done代表迭代是否结束,如果为true说明迭代器中没有更多的值可以取了。

那么迭代器的作用是什么呢?

设想我们写了一个简单的链表数据结构,如下面的代码所示:

class ListNode {  constructor(val) {  this.val = val; this.next = null; } } class LinkedList {  constructor() {  this.head = null; this.length = 0; } append(val) {  const newNode = new ListNode(val); if(!this.head) {  this.head = newNode; } else {  let current = this.head; while(current.next) {  current = current.next; } current.next = newNode; } this.length++; } } 

这个链表有一个缺点,就是遍历起来非常的麻烦,需要声明一个额外的node变量,还要用 while 循环进行判断。

// 创建一个链表,往其中添加了1,2,3三个元素 const linkedList = new LinkedList() linkedList.append(1) linkedList.append(2) linkedList.append(3) // 对链表进行遍历 let node = linkedList.head while (node) {  console.log(node.val) node = node.next } // 依次打印1, 2, 3 

我们希望可以使用这个链表结构的人可以用一种更加自然的方式来进行遍历,比如遍历数组时用的for...of循环,减少开发时的心智负担。这个时候迭代器就可以派上用场了。

// 通过 Iterator 接口实现遍历  [Symbol.iterator]() {  let current = this.head; return {  next: () => {  if (current) {  const value = current.val current = current.next; return {  value, done: false }; } return {  done: true }; } } } 

我们可以给LinkedList类添加一个[Symbol.iterator]属性,它是一个函数,其返回值就是我们一开始所说的迭代器对象,其中包含了next()方法。next()方法会依次返回链表中的值,直到到达链表末尾返回{ done: true }

正确实现了[Symbol.iterator]方法的对象就可以通过for...of来进行遍历了。此时我们就可以像遍历数组一样遍历链表中的内容了。

for (const node of linkedList) {  console.log(node) } // 依次打印1, 2, 3 

不仅如此,我们还可以使用扩展运算符来一次性获取链表中的所有元素并转换为数组。

console.log([...linkedList]) // [1, 2, 3] 

生成器基础

生成器函数和生成器对象

生成器是一个函数,通过在函数名称前面加一个星号*来标识生成器函数,例如:

function* genFunction() {  yield "hello world!"; } 

生成器函数返回了一个生成器对象,生成器对象实现了刚刚所说的迭代器接口,因此它具有next()方法。

let genObject = genFunction(); genObject.next(); // { value: "hello world!", done: false } genObject.next(); // { value: undefined, done: true } 

生成器函数中的yield关键字会暂停函数的执行并返回一个值,return关键字会结束生成器函数的执行。

function* loggerator() {  console.log('开始执行'); yield '暂停'; console.log('继续执行'); return '停止'; } let logger = loggerator(); logger.next(); // 开始执行 // { value: '暂停', done: false } logger.next(); // 继续执行 // { value: '停止', done: true } 

第一次调用next()方法时,生成器函数会执行到第一个yield的位置然后暂停执行,并把对应的值放在value中返回,此时函数还没有return,所以done的值为false

第二次调用next()方法时,生成器函数从第三行开始执行,在return语句处结束执行。函数返回值同样被放在value属性中,但因为此时函数的运行已经结束了,所以done的值为true

因为生成器对象实现了迭代器协议,所以我们可以通过for...of循环和扩展运算符来对其进行操作。

function* abcs() {  yield 'a'; yield 'b'; yield 'c'; } for (let letter of abcs()) {  console.log(letter.toUpperCase()); } // 依次打印 A, B, C [...abcs()] // [ "a", "b", "c" ] 

接收输入

yield除了返回一个值之外,还能用来接收外界的输入。next()方法中传的第一个参数会被yield接收。下面是一个例子:

function* listener() {  console.log("你说,我在听..."); while (true) {  let msg = yield; console.log('我听到你说:', msg); } } let l = listener(); l.next('在吗?'); // 你说,我在听... l.next('你在吗?'); // 我听到你说: 你在吗? l.next('芜湖!'); // 我听到你说: 芜湖! 

第一个next()方法中传入的值会被忽略,这是因为此时函数才刚刚开始执行,还没有遇到任何yield可以接收输入值,函数会在let msg = yield处停止执行,因为遇到了yield关键字。

第二次调用next()时,传入的参数'你在吗?'会被yield语句返回并赋值给变量msg,然后被打印出来。

随后函数再度在let msg = yield处停止执行,这次yield接收到的值是'芜湖!',因此打印出来msg的值就是'芜湖!'

递归生成器

我们在生成器中想要实现递归调用时不能直接使用yield,因为执行生成器函数返回的值是生成器对象,但我们希望返回的是生成出来的值。

通过yield*可以实现生成器函数的递归,这里我们以一个二叉树结构的中序遍历函数为例:

class TreeNode {  constructor(value) {  this.value = value this.leftChild = null this.rightChild = null } // 中序遍历函数,通过yield*实现递归 *[Symbol.iterator]() {  yield this.value if (this.leftChild) yield* this.leftChild if (this.rightChild) yield* this.rightChild } } 

我们构造一个二叉树实例来验证一下我们的中序遍历方法。

const tree = new TreeNode('root') tree.leftChild = new TreeNode("branch-left") tree.rightChild = new TreeNode("branch-right") tree.leftChild.leftChild = new TreeNode("leaf-L1") tree.leftChild.rightChild = new TreeNode("leaf-L2") tree.rightChild.leftChild = new TreeNode("leaf-R1") // root // / \ // branch-left branch-right // / \ / // leaf-L1 leaf-L2 leaf-R1 console.log([...tree]) // ['root', 'branch left', 'leaf L1', 'leaf L2', 'branch right', 'leaf R1'] 

代码可以正确地运行,而且由于我们的二叉树实现了Symbol.iterator,所以我们可以用扩展运算符和for...of循环对其进行遍历。

异步生成器

生成器函数也可以是异步函数。当我们想创建一个异步生成一系列值的对象时就可以使用异步生成器。

async function* count() {  let i = 0; // 每秒产生1个新的数字 while (true) {  // 等待1秒钟 await new Promise(resolve => setTimeout(resolve, 1000)); yield i; i++; } }  (async () => {  let countGenerator = count(); console.log(await countGenerator.next().value); // 1s 后打印 0 console.log(await countGenerator.next().value); // 1s 后打印 1 console.log(await countGenerator.next().value); // 1s 后打印 2 })(); 

在这个例子中,我们声明了一个count函数,其作用是每秒生成一个数字,从 0 开始,每次加 1。由于生成器函数是异步的,所以调用countGenerator.next()得到的会是一个Promise,需要通过await来获取最终生成的值。

除了普通的同步迭代器(也就是上面讲的[Symbol.iterator])之外,JavaScript中的对象还可以声明[Symbol.asyncIterator]属性。声明了asyncIterator的对象可以用for await...of循环进行遍历。

const range = {  from: 1, to: 5, async *[Symbol.asyncIterator]() {  // 生成从 from 到 to 的数值 for(const value = this.from; value <= this.to; value++) {  // 在 value 之间暂停一会儿,等待一些东西 await new Promise(resolve => setTimeout(resolve, 1000)); yield value; } } }; (async () => {  for await (let value of range) {  console.log(value); // 打印 1,然后 2,然后 3,然后 4,然后 5。每个 log 之间会有1s延迟。 } })(); 

这里我们声明了一个 range 对象,设定了其开始数值为 1,结束数值为 5。对其进行迭代可以异步地获取从 1 到 5 的数值。

生成器应用

自定义序列生成

生成器可以帮我们更方便地进行序列的生成。例如我们想要开发一个扑克游戏,需要一个包含了所有扑克牌数值的序列,如果一一列举会非常麻烦,用生成器就可以简化我们的工作。

const cards = ({  suits: ["♣️", "♦️", "♥️", "♠️"], court: ["J", "Q", "K", "A"], [Symbol.iterator]: function* () {  for (let suit of this.suits) {  for (let i = 2; i <= 10; i++) yield suit + i; for (let c of this.court) yield suit + c; } } }) 

我们在cards[Symbol.iterator]中对四种花色进行遍历,对于每一种花色,我们首先从2数到10,并数字字符与花色的字符进行拼接,从而生成该花色对应的所有数字牌,共九张;随后再遍历四个特殊的字母字符"J", "Q", "K", "A",与花色的字符进行拼接,生成剩余的四张牌。

console.log([...cards]) // ['♣️2', '♣️3', '♣️4', '♣️5', '♣️6', '♣️7', '♣️8', '♣️9', '♣️10', '♣️J', '♣️Q', '♣️K', '♣️A', '♦️2', '♦️3', '♦️4', '♦️5', '♦️6', '♦️7', '♦️8', '♦️9', '♦️10', '♦️J', '♦️Q', '♦️K', '♦️A', '♥️2', '♥️3', '♥️4', '♥️5', '♥️6', '♥️7', '♥️8', '♥️9', '♥️10', '♥️J', '♥️Q', '♥️K', '♥️A', '♠️2', '♠️3', '♠️4', '♠️5', '♠️6', '♠️7', '♠️8', '♠️9', '♠️10', '♠️J', '♠️Q', '♠️K', '♠️A'] 

使用扩展运算符就可以一次性取出cards中的所有扑克牌数值并转化为数组了,这样的写法比直接一一列举方便了很多。

惰性计算

因为生成器函数执行时会在yield处停止,所以即便是while(true)这样的死循环也不会导致程序卡死,我们可以写一个不断生成随机数的generateRandomNumbers函数。

function* generateRandomNumbers(count) {  for (let i = 0; i < count; i++) {  yield Math.random() } } 

惰性计算的含义是在要用到的时候才进行求值。以上面的generateRandomNumbers函数为例,如果我们采用普通的函数写法,调用函数时JS引擎会一次性把所有随机数都计算出来,如果传入的count过大的话,容易造成应用卡顿。

如果采用生成器的写法,我们可以一次从生成器中取出一个随机数,这样每次只进行了一次随机数生成的操作,避免了过度的性能消耗。

状态机

利用生成器可以接受输入的特性,我们可以通过生成器函数来构建一个状态机。

function* bankAccount() {  let balance = 0; while (balance >= 0) {  balance += yield balance; } return '你破产了!'; } let account = bankAccount(); account.next(); // { value: 0, done: false } account.next(50); // { value: 50, done: false } account.next(-10); // { value: 40, done: false } account.next(-60); // { value: "你破产了!", done: true } 

这里我们构造了一个银行账户的生成器,一开始用户的余额是0,我们可以通过在account.next()传入账户变动的数值来改变余额。如果余额变成了负数,就结束函数执行并告知用户破产。

简化分页请求

目前,有很多在线服务都是发送的分页的数据(paginated data)。例如,当我们需要一个用户列表时,一个请求只返回一个预设数量的用户(例如 100 个用户)—— “一页”,并提供了指向下一页的 URL。

星球大战API为例,其中的planetsAPI中包含了星球大战中的各个行星的信息。这是一个分页的API,每次请求返回 60 个星球的数据,包含在 results 字段中,下一页的链接在next字段中。

image-20230607214000904.png

直接请求这个接口来迭代地获取每一页的行星数据是非常麻烦的。所以我们希望对这个接口进行封装,创建一个函数 fetchPlanets(),我们每次调用这个函数就可以获得下一页的 Planets 数据,并且可以使用 for await..of 来迭代获取所有行星数据。其代码如下所示:

async function* fetchPlanets() {  let nextUrl = `https://swapi.dev/api/planets`; while (nextUrl) {  const response = await fetch(nextUrl); const data = await response.json(); nextUrl = data.next; yield data.results; } } 

调用这个函数,我们可以得到一个planetPages生成器对象,对这个对象进行for await..of 迭代可以得到每一页的行星数据。可以看到,通过异步生成器进行请求封装大大简化了我们的异步请求代码。

const planetPages = fetchPlanets(); (async () => {  for await (const page of planetPages) {  console.log(page); } })(); 

代码运行的效果如图所示:

ezgif-5-eb1465aed6.gif

总结

本文中的例子和思路参考了 Anjana Vakil 的演讲以及现代 JavaScript 教程中的相关章节

本文讲解了JavaScript中生成器的概念和相关应用场景。

  • 生成器是一个函数,调用生成器函数可以得到一个生成器对象,生成器对象是一种迭代器(Iterator)。可以用扩展运算符...来一次性获取所有生成值,也可以用for...of循环来遍历生成器生成的值。
  • 生成器可以通过在next()函数调用时传递参数来接受输入。
  • 生成器可以通过yield*关键字进行递归调用。
  • 生成器可以是异步函数,异步生成器对象可以用for await...of循环进行遍历。
  • 利用生成器可以实现自定义序列的生成、惰性计算、状态机和异步分页请求的优雅封装。

本文作者 wzkMaster, 如果有帮助的话欢迎点赞收藏~

相关文章
|
6月前
|
移动开发 前端开发 JavaScript
征信报告修改器,征信报告生成器,制作软件无痕修改软件【js+html+css】
本项目为信用评分模拟器教学工具,采用HTML5实现,仅供学习参考。核心功能通过JavaScript构建,包含虚拟数据生成、权重分配及信用因素分析(如还款记录、信用使用率等)。
|
6月前
|
存储 自然语言处理 前端开发
抖音快手小红书虚拟评论截图生成器,模拟对话制作工具,html+js+css
这是一款纯前端实现的多平台虚拟评论生成器,支持抖音、快手、小红书风格,适用于产品演示与UI设计。采用Vanilla JS与Flexbox布局,利用IndexedDB存储数据,CSS Variables切换主题。
|
2月前
|
前端开发 JavaScript API
js实现promise常用场景使用示例
本文介绍JavaScript中Promise的6种常用场景:异步请求、定时器封装、并行执行、竞速操作、任务队列及与async/await结合使用,通过实用示例展示如何优雅处理异步逻辑,避免回调地狱,提升代码可读性与维护性。
251 10
|
3月前
|
JavaScript 前端开发 开发者
Nest.js控制器深度解析:路由与请求处理的高级特性
以上就是对 NestJS 控制层高级特性深度解析:从基本概念到异步支持再到更复杂场景下拦截其与管道等功能性组件运用都有所涉及,希望能够帮助开发者更好地理解和运用 NestJS 进行高效开发工作。
361 15
|
3月前
|
JavaScript 前端开发 IDE
TypeScript vs. JavaScript:技术对比与核心差异解析
TypeScript 作为 JavaScript 的超集,通过静态类型系统、编译时错误检测和强大的工具链支持,显著提升代码质量与可维护性,尤其适用于中大型项目和团队协作。相较之下,JavaScript 更灵活,适合快速原型开发。本文从类型系统、错误检测、工具支持等多维度对比两者差异,并提供技术选型建议,助力开发者合理选择。
798 1
|
3月前
|
存储 JavaScript 前端开发
JavaScript 语法全面解析
JavaScript 语法体系丰富且不断更新,从基础的变量声明、数据类型,到复杂的函数、对象、异步语法,每个知识点都需要开发者深入理解并灵活运用。本文梳理的 JS 语法核心内容,可为开发者提供系统的学习框架,后续还需通过大量实践(如编写交互组件、实现业务逻辑)巩固知识,逐步提升 JS 编程能力,应对前端开发中的各类挑战。
|
6月前
|
机器学习/深度学习 JavaScript 前端开发
JS进阶教程:递归函数原理与篇例解析
通过对这些代码示例的学习,我们已经了解了递归的原理以及递归在JS中的应用方法。递归虽然有着理论升华,但弄清它的核心思想并不难。举个随手可见的例子,火影鸣人做的影分身,你看到的都是同一个鸣人,但他们的行为却能在全局产生影响,这不就是递归吗?雾里看花,透过其间你或许已经深入了递归的魅力之中。
286 19
|
6月前
|
存储 前端开发 安全
病历单生成器在线制作,病历单生成器app,HTML+CSS+JS恶搞工具
本项目为医疗病历模拟生成器,旨在为医学教学和软件开发测试提供数据支持,严格遵守《医疗机构病历管理规定》。
|
6月前
|
前端开发 容器
处方单图片生成器, 处方单在线制作免费,js+css+html恶搞神器
这是一个电子处方模拟生成系统,使用html2canvas库实现图片导出功能。系统生成的处方单包含多重防伪标识,并明确标注为模拟数据,仅供学习

热门文章

最新文章

推荐镜像

查看更多
  • DNS
  • 下一篇