ES6深度解析:Generators

2024-01-16 06:58

本文主要是介绍ES6深度解析:Generators,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者 | Max力出奇迹

来源 | https://www.cnblogs.com/welody/p/14980736.html

介绍ES6 Generators

什么是Generators(生成器函数)?让我们先来看看一个例子。

function* quips(name) {  yield "hello " + name + "!";  yield "i hope you are enjoying the blog posts";  if (name.startsWith("X")) {    yield "it's cool how your name starts with X, " + name;  }  yield "see you later!";}

这是一只会说话的猫的一些代码,可能是当今互联网上最重要的一种应用。它看起来有点像一个函数,对吗?这被称为生成器-函数,它与函数有很多共同之处。但你马上就能看到两个不同之处。

普通函数以function开头,生成器函数以function*开头。

在生成器函数中,yield是一个关键字,语法看起来像return。不同的是,函数(甚至是生成器函数)只能返回一次,而生成器函数可以“yield”任何次数。yield表达式暂停了生成器的执行,同时它可以在以后再次恢复。

Generators可以做什么

当你调用生成器-函数quips()时会发生什么?

> var iter = quips("jorendorff");  [object Generator]> iter.next()  { value: "hello jorendorff!", done: false }> iter.next()  { value: "i hope you are enjoying the blog posts", done: false }> iter.next()  { value: "see you later!", done: false }> iter.next()  { value: undefined, done: true }

你可能已经非常习惯于普通函数和它们的行为方式。当你调用它们时,它们会立即开始运行,并一直运行到返回或抛出异常。

所有这些对任何js程序员来说都是第二天性。

调用一个生成器看起来也是一样的:quips("jorendorff")。但是当你调用一个生成器时,它还没有开始运行。

相反,它返回一个暂停的Generator对象iter(就是在上面的例子中叫做iter的对象)。

你可以把这个Generator对象看作是一个函数调用,在调用前被冻结。

具体来说,它被冻结在生成器函数的顶端,就在运行其第一行代码之前。每次你调用Generator对象的方法.next()时,函数调用都会自我解冻,并运行到下一个yield表达式为止。

这就是为什么我们每次调用上面的iter.next()方法,都会得到一个不同的字符串值。这些都是由函数quips()中的yield表达式产生的值。

在最后一次iter.next()调用中,我们终于到达了生成器-函数的终点,所以结果中.done字段的值是true。到达生成器函数的终点就像普通函数返回undefined一样,这就是为什么结果的value字段值是undefined。

现在可能是一个好时机,回到会说话的猫的演示页面,真正地玩一玩代码。试着把yield放在一个循环里面。会发生什么?从技术上讲,每次Generator执行yield时,它的堆栈--局部变量、参数、临时值以及当前在Generator主体中的执行位置--都会从堆栈中删除。

然而,Generator对象会保留对这个堆栈框架的引用(或副本),以便以后.next()调用可以重新激活它并继续执行。

值得指出的是,Generator不是线程。在有线程的语言中,多段代码可以同时运行,通常会导致竞赛条件、非确定性和甜蜜的性能。Generator则完全不是这样的。

当一个Generator运行时,它与调用者在同一个线程中运行。执行的顺序是顺序的、确定的,而不是并发的。与系统线程不同,Generator只在其函数体中标明的yield点上暂停运行。

好了。我们知道Generator是什么。我们已经看到了一个Generator的运行,暂停自己,然后恢复执行。现在有个大问题。

这种奇怪的能力怎么可能有用?

Generators就是迭代器(Generators are iterators)

ES6迭代器不仅仅是一个单一的内置类。它们是该语言的一个扩展点。你可以通过实现两个方法Symbol.iterator和next()来创建你自己的迭代器。

但是实现一个接口至少要做一点工作。让我们看看迭代器的实现在实践中是什么样的。

作为一个例子,让我们做一个简单的迭代器range,它只是从一个数字到另一个数字进行计数,就像一个老式的C循环for 一样。

// This should "ding" three timesfor (var value of range(0, 3)) {  alert("Ding! at floor #" + value);}

这里有一个解决方案,使用ES6类class。

class RangeIterator {  constructor(start, stop) {    this.value = start;    this.stop = stop;  }[Symbol.iterator]() { return this; }next() {    var value = this.value;    if (value < this.stop) {      this.value++;      return {done: false, value: value};    } else {      return {done: true, value: undefined};    }  }}
// Return a new iterator that counts up from 'start' to 'stop'.function range(start, stop) {  return new RangeIterator(start, stop);}

这就是在Java或Swift中实现迭代器的情况。这并不坏。但也不完全是微不足道的。这段代码里有什么错误吗?这可不好说。

它看起来完全不像我们在这里试图模仿的原始循环:for ,迭代器协议迫使我们拆除了循环。在这一点上,你可能对迭代器感到有点冷淡。

它们可能很好用,但似乎很难实现。

你可能不会想建议我们在js语言中引入一个疯狂的、令人费解的新控制流结构,只是为了使迭代器更容易构建。

但既然我们有生成器Generator,我们能在这里使用它们吗?让我们试试吧。

function* range(start, stop) {  for (var i = start; i < stop; i++)    yield i;}

上面的4行range()代码可以直接替代以前的23行实现,包括整个类RangeIterator。

就是因为Generator是迭代器,所以这一切才是可能的。

所有的生成器都有一个内置的next()和Symbol.iterator的实现。你只需写出循环的行为。

在没有Generator的情况下实现迭代器,就像被迫完全用被动语态来写一封长邮件。

当简单地说出你的意思不是一个选项时,你最终说的东西可能会变得相当复杂。"我的意思是,我的意思是,我必须在不使用循环语法的情况下描述一个循环的功能,所以RangeIterator又长又奇怪。而Generator就是答案。

我们还可以如何利用生成器作为迭代器的能力呢?

让任何对象都可以迭代。只需写一个Generator函数来遍历this,在遍历时产生(yield)每个值。

然后把这个生成器函数设置为this对象的[Symbol.iterator]方法。

简化建数组函数。假设你有一个函数,每次调用都会返回一个数组的结果,就像下面这个函数:

//将一维数组'图标'切分成长度为'rowLength'的数组function splitIntoRows(icons, rowLength) {  var rows = [];  for (var i = 0; i < icons.length; i += rowLength) {    rows.push(icons.slice(i, i + rowLength));  }  return rows;}

使用Generator会让这种代码更短一些。

function* splitIntoRows(icons, rowLength) {  for (var i = 0; i < icons.length; i += rowLength) {    yield icons.slice(i, i + rowLength);  }}

执行时唯一区别是,它不是一次性计算所有的结果并返回一个数组,而是返回一个迭代器,然后根据需要逐个计算结果。

异常大小的结果。你不可能建立一个无限的数组。但是你可以返回一个Generator,生成一个无尽的序列,每个调用者可以从其中提取他们需要的任何数量的值。

重构复杂的循环。你有一个巨大的丑陋的函数吗?你想把它分解成两个更简单的部分吗?Generator是添加到你的重构工具箱中的一把新刀。

当你面对一个复杂的循环时,你可以把代码中产生数据的部分分解出来,把它变成一个单独的生成器-函数。

然后将循环改为:for (var data of myNewGenerator(args))

处理可迭代数据的工具。ES6并没有提供一个扩展库,用于过滤、映射,以及一般情况下对任意的可迭代数据集进行任意的处理。

但是Generator对于构建你所需要的工具来说是非常棒的,只需要几行代码。

例如,假设你需要一个新的在DOM NodeLists上遍历的方法,而不仅仅是Arrays。

小菜一碟:创建Array.prototype.filter

function* filter(test, iterable) {  for (var item of iterable) {    if (test(item))      yield item;  }}

那么Generator是否有用呢?当然,它们是实现自定义迭代器的一种惊人的简单方法,而且迭代器是整个ES6的数据和循环的新标准。

但这并不是Generator的全部功能。这甚至可能不是它们所做的最重要的事情。

生成器与异步代码(Generators and asynchronous code)

下面是我前段时间写的一些JS代码。

          };        })      });    });  });});

也许你已经在自己的代码中看到了这样的东西。异步API通常需要一个回调,这意味着每次你做什么都要写一个额外的匿名函数。

因此,如果你有一点代码做三件事,而不是三行代码,你就会看到三个缩进层次的代码。

下面是我写的一些更多的JS代码。

}).on('close', function () {  done(undefined, undefined);}).on('error', function (error) {  done(error);});

异步API有错误处理惯例,而不是异常。不同的API有不同的约定。在大多数API中,默认情况下,错误会被默默地放弃。

在一些API中,即使是普通的成功完成也是默认放弃的。

直到现在,这些问题都是我们为异步编程付出的代价。我们已经接受了这样的事实:异步代码看起来并不像相应的同步代码那样漂亮和简单。

Generator提供了新的希望:我们不必再写那样丑陋的代码。

Q.async()是一个实验性的尝试,它使用Generators与Promises来产生类似于相应同步代码的异步代码。比如说:

// Synchronous code to make some noise.function makeNoise() {  shake();  rattle();  roll();}
// Asynchronous code to make some noise.// Returns a Promise object that becomes resolved// when we're done making noise.function makeNoise_async() {  return Q.async(function* () {    yield shake_async();    yield rattle_async();    yield roll_async();  });}

主要的区别是,异步版本必须在调用异步函数的每个地方添加关键字yield。在该版本中添加像语句if或try/catch块这样的代码,就像在普通的同步版本中添加它一样。

与其他编写异步代码的方式相比,这感觉不像是在学习一种全新的语言。

因此,Generator为一种新的异步编程模型指明了方向,它似乎更适合人类的大脑。这项工作正在进行中。在其他方面,更好的语法可能会有帮助。

一项关于异步函数的建议,建立在Promises和Generators的基础上,并从C#的类似功能中获得灵感,已被提上ES7的议程。

学习更多技能

请点击下方公众号

这篇关于ES6深度解析:Generators的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/611669

相关文章

HTML5的input标签的`type`属性值详解和代码示例

《HTML5的input标签的`type`属性值详解和代码示例》HTML5的`input`标签提供了多种`type`属性值,用于创建不同类型的输入控件,满足用户输入的多样化需求,从文本输入、密码输入、... 目录一、引言二、文本类输入类型2.1 text2.2 password2.3 textarea(严格

Agent开发核心技术解析以及现代Agent架构设计

《Agent开发核心技术解析以及现代Agent架构设计》在人工智能领域,Agent并非一个全新的概念,但在大模型时代,它被赋予了全新的生命力,简单来说,Agent是一个能够自主感知环境、理解任务、制定... 目录一、回归本源:到底什么是Agent?二、核心链路拆解:Agent的"大脑"与"四肢"1. 规划模

MySQL字符串转数值的方法全解析

《MySQL字符串转数值的方法全解析》在MySQL开发中,字符串与数值的转换是高频操作,本文从隐式转换原理、显式转换方法、典型场景案例、风险防控四个维度系统梳理,助您精准掌握这一核心技能,需要的朋友可... 目录一、隐式转换:自动但需警惕的&ld编程quo;双刃剑”二、显式转换:三大核心方法详解三、典型场景

SpringBoot返回文件让前端下载的几种方式

《SpringBoot返回文件让前端下载的几种方式》文章介绍了开发中文件下载的两种常见解决方案,并详细描述了通过后端进行下载的原理和步骤,包括一次性读取到内存和分块写入响应输出流两种方法,此外,还提供... 目录01 背景02 一次性读取到内存,通过响应输出流输出到前端02 将文件流通过循环写入到响应输出流

SpringBoot+Vue3整合SSE实现实时消息推送功能

《SpringBoot+Vue3整合SSE实现实时消息推送功能》在日常开发中,我们经常需要实现实时消息推送的功能,这篇文章将基于SpringBoot和Vue3来简单实现一个入门级的例子,下面小编就和大... 目录前言先大概介绍下SSE后端实现(SpringBoot)前端实现(vue3)1. 数据类型定义2.

SQL 注入攻击(SQL Injection)原理、利用方式与防御策略深度解析

《SQL注入攻击(SQLInjection)原理、利用方式与防御策略深度解析》本文将从SQL注入的基本原理、攻击方式、常见利用手法,到企业级防御方案进行全面讲解,以帮助开发者和安全人员更系统地理解... 目录一、前言二、SQL 注入攻击的基本概念三、SQL 注入常见类型分析1. 基于错误回显的注入(Erro

前端Visual Studio Code安装配置教程之下载、汉化、常用组件及基本操作

《前端VisualStudioCode安装配置教程之下载、汉化、常用组件及基本操作》VisualStudioCode是微软推出的一个强大的代码编辑器,功能强大,操作简单便捷,还有着良好的用户界面,... 目录一、Visual Studio Code下载二、汉化三、常用组件1、Auto Rename Tag2

C++ 多态性实战之何时使用 virtual 和 override的问题解析

《C++多态性实战之何时使用virtual和override的问题解析》在面向对象编程中,多态是一个核心概念,很多开发者在遇到override编译错误时,不清楚是否需要将基类函数声明为virt... 目录C++ 多态性实战:何时使用 virtual 和 override?引言问题场景判断是否需要多态的三个关

Springboot主配置文件解析

《Springboot主配置文件解析》SpringBoot主配置文件application.yml支持多种核心值类型,包括字符串、数字、布尔值等,文章详细介绍了Profile环境配置和加载位置,本文... 目录Profile环境配置配置文件加载位置Springboot主配置文件 application.ym

Java枚举类型深度详解

《Java枚举类型深度详解》Java的枚举类型(enum)是一种强大的工具,它不仅可以让你的代码更简洁、可读,而且通过类型安全、常量集合、方法重写和接口实现等特性,使得枚举在很多场景下都非常有用,本文... 目录前言1. enum关键字的使用:定义枚举类型什么是枚举类型?如何定义枚举类型?使用枚举类型:2.