深入解析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, 如果有帮助的话欢迎点赞收藏~

相关文章
|
机器学习/深度学习 文字识别 监控
安全监控系统:技术架构与应用解析
该系统采用模块化设计,集成了行为识别、视频监控、人脸识别、危险区域检测、异常事件检测、日志追溯及消息推送等功能,并可选配OCR识别模块。基于深度学习与开源技术栈(如TensorFlow、OpenCV),系统具备高精度、低延迟特点,支持实时分析儿童行为、监测危险区域、识别异常事件,并将结果推送给教师或家长。同时兼容主流硬件,支持本地化推理与分布式处理,确保可靠性与扩展性,为幼儿园安全管理提供全面解决方案。
588 3
|
7月前
|
前端开发 JavaScript API
js实现promise常用场景使用示例
本文介绍JavaScript中Promise的6种常用场景:异步请求、定时器封装、并行执行、竞速操作、任务队列及与async/await结合使用,通过实用示例展示如何优雅处理异步逻辑,避免回调地狱,提升代码可读性与维护性。
382 10
|
人工智能 API 开发者
HarmonyOS Next~鸿蒙应用框架开发实战:Ability Kit与Accessibility Kit深度解析
本书深入解析HarmonyOS应用框架开发,聚焦Ability Kit与Accessibility Kit两大核心组件。Ability Kit通过FA/PA双引擎架构实现跨设备协同,支持分布式能力开发;Accessibility Kit提供无障碍服务构建方案,优化用户体验。内容涵盖设计理念、实践案例、调试优化及未来演进方向,助力开发者打造高效、包容的分布式应用,体现HarmonyOS生态价值。
837 27
|
数据采集 前端开发 JavaScript
金融数据分析:解析JavaScript渲染的隐藏表格
本文详解了如何使用Python与Selenium结合代理IP技术,从金融网站(如东方财富网)抓取由JavaScript渲染的隐藏表格数据。内容涵盖环境搭建、代理配置、模拟用户行为、数据解析与分析等关键步骤。通过设置Cookie和User-Agent,突破反爬机制;借助Selenium等待页面渲染,精准定位动态数据。同时,提供了常见错误解决方案及延伸练习,帮助读者掌握金融数据采集的核心技能,为投资决策提供支持。注意规避动态加载、代理验证及元素定位等潜在陷阱,确保数据抓取高效稳定。
484 17
|
数据采集 机器学习/深度学习 存储
可穿戴设备如何重塑医疗健康:技术解析与应用实战
可穿戴设备如何重塑医疗健康:技术解析与应用实战
698 4
|
存储 弹性计算 安全
阿里云服务器ECS通用型规格族解析:实例规格、性能基准与场景化应用指南
作为ECS产品矩阵中的核心序列,通用型规格族以均衡的计算、内存、网络和存储性能著称,覆盖从基础应用到高性能计算的广泛场景。通用型规格族属于独享型云服务器,实例采用固定CPU调度模式,实例的每个CPU绑定到一个物理CPU超线程,实例间无CPU资源争抢,实例计算性能稳定且有严格的SLA保证,在性能上会更加稳定,高负载情况下也不会出现资源争夺现象。本文将深度解析阿里云ECS通用型规格族的技术架构、实例规格特性、最新价格政策及典型应用场景,为云计算选型提供参考。
|
人工智能 自然语言处理 算法
DeepSeek大模型在客服系统中的应用场景解析
在数字化浪潮下,客户服务领域正经历深刻变革,AI技术成为提升服务效能与体验的关键。DeepSeek大模型凭借自然语言处理、语音交互及多模态技术,显著优化客服流程,提升用户满意度。它通过智能问答、多轮对话引导、多模态语音客服和情绪监测等功能,革新服务模式,实现高效应答与精准分析,推动人机协作,为企业和客户创造更大价值。
1032 5
|
存储 JavaScript 前端开发
全网最全情景,深入浅出解析JavaScript数组去重:数值与引用类型的全面攻略
如果是基础类型数组,优先选择 Set。 对于引用类型数组,根据需求选择 Map 或 JSON.stringify()。 其余情况根据实际需求进行混合调用,就能更好的实现数组去重。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
人工智能 自然语言处理 算法
DeepSeek 大模型在合力亿捷工单系统中的5大应用场景解析
工单系统是企业客户服务与内部运营的核心工具,传统系统在分类、派发和处理效率方面面临挑战。DeepSeek大模型通过自然语言处理和智能化算法,实现精准分类、智能分配、自动填充、优先级排序及流程优化,大幅提升工单处理效率和质量,降低运营成本,改善客户体验。
728 2
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

推荐镜像

更多
  • DNS