[译]JavaScript Symbols, Iterators, Generators, Async/Await, and Async Iterators 

1,439 阅读15分钟

原文地址:简单解释JavaScript Symbols, Iterators, Generators, Async/Await, and Async Iterators

某些JavaScript(ECMAScript)功能比其他功能更容易理解。Generators看起来很奇怪-- 就像C/C ++中的指针一样。Symbols看起来像原始值和对象。

这些功能都是相互关联的,并且相互构建。所以如果不理解一件事你就无法理解另一件事

所以在本文中,将介绍symbols,global-symbols,iterators,iterables,generators ,async/await 和async iterators。将首先解释“ 为什么 ”,并通过一些有用的例子展示他们是如何工作的。

这是一个相对高阶的主题,但它并不复杂。本文应该让你很好地掌握所有这些概念。

好的,让我们开始吧

Symbols

在ES2015中,创建了一个新的(第6个)数据类型symbol

为什么?

三个主要原因是:

原因1 - 添加具有向后兼容性的新核心功能

JavaScript开发人员和ECMAScript委员会(TC39)需要一种方法来添加新的对象属性,而不会破坏现有方法像for...in循环或JavaScript方法像Object.keys

例如,如果一个对象,var myObject = {firstName:'raja', lastName:'rao'} 运行Object.keys(myObject)它将返回[firstName, lastName]

现在,如果我们添加另一个属性,为myObject设置newProperty ,如果运行Object.keys(myObject)它应该仍然返回旧值(即,以某种方式使之忽略新加入的newproperty),并且只显示[firstName, lastName]而不是[firstName, lastName, newProperty] 。这该如何做?

我们之前无法真正做到这一点,因此创建了一个名为Symbols的新数据类型。

如果作为symbol添加newProperty,那么Object.keys(myObject)会忽略它(因为它不识别它),仍然返回[firstName, lastName]

原因2 - 避免名称冲突

他们还希望保持这些属性的独特性。通过这种方式,可以继续向全局添加新属性(并且可以添加对象属性),而无需担心名称冲突。

例如,假设有一个自定义的对象,将在对象中将自定义toUpperCase函数添加到全局Array.prototype

现在,假设加载了另一个库(或着ES2019发布的库)并且它有与自定义函数不同的Array.prototype.toUpperCase.然后自定义函数可能会因名称冲突而中断。

那怎么解决这个可能不知道的名称冲突?这就是Symbols用武之地。它们在内部创建了独特的值,允许创建添加属性而不必担心名称冲突。

原因3 - 通过“众所周知的”符号(“Well-known” Symbols)允许钩子调用核心方法

假设需要一些核心方法,比如String.prototype.search调用自定义函数。也就是说,‘somestring’.search(myObject);应该调用myObject的搜索函数并将 ‘somestring’作为参数传递,我们该怎么做?

这就是ES2015提出了一系列称为“众所周知”的Symbols的全局Symbols。只要你的对象将其中一个Symbols作为属性,就可以重新定位核心函数来调用你的函数。

我们现在先不谈论这个问题,将在本文后面详细介绍所有细节。但首先,让我们了解Symbols实际是如何工作的。

创建Symbols

可以通过调用Symbol全局函数/对象来创建符号Symbol 。该函数返回数据类型的值symbol

注意:Symbols可能看起来像对象,因为它们有方法,但它们不是 - 它们是原始的。可以将它们视为与常规对象具有某些相似性的“特殊”对象,但它们的行为与常规对象不同。

例如:Symbols具有与对象类似的方法,但与对象不同,它们是不可变的且唯一的。

“new”关键字无法创建Symbols

因为Symbols不是对象而new关键字应该返回Object,所以我们不能通过new来返回symbols 数据类型。

var mySymbol = new Symbol(); //抛出错误

Symbols有“描述”

Symbols可以有描述 - 它只是用于记录目的。

// mySymbol变量现在包含一个“Symbols”唯一值
//它的描述是“some text” 
const mySymbol = Symbol('some text');

Symbols是唯一的

const mySymbol1 =Symbols('some text'); 
const mySymbol2 =Symbols('some text'); 
mySymbol1 == mySymbol2 // false

如果我们使用“Symbol.for”方法,Symbols表现的就像一个单例

如果不通过Symbol()创建Symbol,可以通过Symbol.for(<key>)创建symbol, 。这需要一个“key”(字符串)来创建一个Symbol。如果一个key对应的Symbol已经存在,它只返回旧Symbol。因此,如果我们使用该Symbol.for方法,它就像一个单例。

var mySymbol1 = Symbol .for('some key'); //创建一个新symbol
var mySymbol2 = Symbol .for('some key'); // ** 返回相同的symbol
 mySymbol1 == mySymbol2 // true

使用 .for真正原因是在一个地方创建一个Symbols,并从其他地方访问相同的Symbols。

注意Symbol.for如果键是相同的,将覆盖之前的值,这将使Symbol非唯一,所以尽可能避免这种情况。

Symbols的key与描述

若只是为了让事情更清楚,如果不使用Symbol.for ,那么Symbol是唯一的。但是,如果使用Symbol.for,而且key 不是唯一的,则返回的Symbol也不是唯一的。

Symbols可以是一个对象属性键

这对于Symbols来说是一个非常独特的东西———— 也是最令人困惑的。虽然它们看起来像一个对象,但它们是原始的。我们可以将Symbol作为属性键添加到对象,就像String一样。

实际上,这是使用Symbols的主要方式之一 ,作为对象属性。

注意:使用Symbols的对象属性是“键属性”。

[]操作符与.操作符

不能使用.操作符,因为.操作符仅适用于字符串属性,因此应使用[]操作符。

使用Symbol的3个主要原因 - review

让我们回顾一下的三个主要原因来了解Symbol是如何工作的。

原因1 - Symbols对于循环和其他方法是不可见的

下面示例中使用for-in循环遍历一个对象obj,但它不知道(或忽略)prop3,prop4因为它们是symbols。

下面是另一个示例,其中Object.keysObject.getOwnPropertyNames忽略了Symbol的属性名称。

原因2 - Symbols是唯一的

假设想要在全局Array对象上调用Array.prototype.includes的功能。它将与JavaScript(ES2018)默认方法includes冲突。如何在不冲突的情况下添加它?

首先,创建一个具有合适名称的变量includes并为其指定一个symbol。然后将此变量(现在是symbol)添加到全局Array使用括号表示法。分配想要的任何功能。

最后使用括号表示法调用该函数。但请注意,必须在括号内传递实际symbol,如:arr[includes]()而不是字符串。

原因3-众所周知的Symbols(即“全局”symbols)

默认情况下,JavaScript会自动创建一堆Symbols变量并将它们分配给全局Symbol对象(使用相同的Symbol()去创建Symbols)。

ECMAScript 2015,这些Symbols被加入到核心对象如数组和字符串的核心方法如String.prototype.searchString.prototype.replace

这些symbols的一些例子是:Symbol.match,Symbol.replace,Symbol.search,Symbol.iterator和Symbol.split

由于这些全局Symbols是全局的并且是公开的,我们可以使用核心方法调用自定义函数而不是内部函数。

一个例子: Symbol.search

例如,String对象的String.prototype.search公共方法搜索regExp或字符串,并返回索引(如果找到)。

在ES2015中,它首先检查是否在查询regExp(RegExp对象)中实现了Symbol.search方法。如果实现了,那么它调用该函数并将工作委托给它。像RegExp这样的核心对象实现了实际完成工作的SymbolSymbol.search

Symbol.search的内部工作原理

  1. 解析 ‘rajarao’.search(‘rao’);
  2. “rajarao”转换为String对象 new String(“rajarao”)
  3. “rao”转换为RegExp对象 new Regexp(“rao”)
  4. 调用字符串对象“rajarao”的方法search,传递'rao'对象为参数。
  5. search方法调用“rao”对象内部方法Symbol.search(将搜索委托返回“rao”对象)并传递“rajarao”。像这样:"rao"[Symbol.search]("rajarao")
  6. "rao"[Symbol.search]("rajarao")返回索引结果4传递给search函数,最后,search返回4到我们的代码。

下面的伪代码片段显示了代码内部的工作方式:

不是一定需要通过RegExp。可以传递任何实现Symbol.search并返回任何所需内容的自定义对象。

自定义String.search方法来调用自定义函数

下面的例子展示了我们如何使String.prototype.search调用自定义Product类的搜索功能 - 多亏了Symbol.search全局Symbol

Symbol.search(CUSTOM BEHAVIOR)的内部工作原理

  1. 解析 ‘barsoap’.search(soapObj);
  2. “barsoap”转换为String对象new String(“barsoap”)
  3. 由于soapObj已经是对象,不要进行任何转换
  4. 调用“barsoap”字符串对象的search方法。
  5. search方法调用“soapObj”对象内部方法Symbol.search(它将搜索委托回“soapObj”对象)并传递“barsoap”作为参数。像这样:soapObj[Symbol.search]("barsoap")
  6. soapObj[Symbol.search]("barsoap")返回索引结果FOUNDsearch函数,最后,search返回FOUND到我们的代码。

好的,让我们转到Iterators。

迭代器和Iterables

为什么?

在几乎所有的应用程序中,我们都在不断处理数据列表,我们需要在浏览器或移动应用程序中显示这些数据。通常我们编写自己的方法来存储和提取数据。

但问题是,我们已经有了for-of循环和扩展运算符(…)等标准方法来从标准对象(如数组,字符串和映射)中提取数据集合。为什么我们不能将这些标准方法用于我们的Object?

在下面的示例中,我们不能使用for-of循环或(…)运算符来从Users类中提取数据。我们必须使用自定义get方法。

但是,能够在我们自己的对象中使用这些现有方法不是很好吗?为了实现这一点,我们需要制定所有开发人员可以遵循的规则,并使其对象与现有方法一起使用。

如果他们遵循这些规则从对象中提取数据,那么这些对象称为“迭代”。

规则是:

  1. 主对象/类应该存储一些数据。
  2. 主对象/类必须具有全局“众所周知的”Symbolssymbol.iterator作为其属性,Symbols根据规则#3至#6实现特定方法。
  3. symbol.iterator方法必须返回另一个对象 - “迭代器”对象。
  4. 这个“迭代器”对象必须有一个称为next的方法。
  5. next方法应该可以访问存储在规则1中的数据。
  6. 如果我们调用iteratorObj.next(),它应该返回规则#1中的一些存储数据无论是想要返回更多值{value:<stored data>, done: false},还是不想返回任何数据{done: true}

如果遵循所有这6个规则,则来自规则#1的主要对象被称为 可迭代。 它返回的对象称为迭代器

我们来看看如何创建Users对象和迭代:

重要说明:如果我们传递一个iterable(allUsers)for-of 循环或扩展运算符,将会在内部调用<iterable>[Symbol.iterator]()获取迭代器(如allUsersIterator),然后使用迭代器提取数据。

所以在某种程度上,所有这些规则都有一个返回iterator对象的标准方法。

Generator 函数

为什么?

主要有两个原因:

  1. 为迭代提供更高级别的抽象
  2. 提供更新的控制流来帮助解决诸如“回调地狱”之类的问题。

我们来看看它的详细内容。

原因1 - 迭代的包装器

不是通过遵循所有这些规则来使我们的类/对象成为一个iterable,我们可以简单地创建一个“Generator”方法来简化这件事情。

以下是关于Generator的一些要点:

  1. Generator方法在内部有一个*<myGenerator>新语法,Generator函数有语法function * myGenerator(){}
  2. 调用generatormyGenerator()返回一个实现iterator协议(规则)的generator对象,因此我们可以将其用作iterator开箱即用的返回值。
  3. generator使用特殊yield语句来返回数据。
  4. yield 语句保持以前的调用状态,并从它停止的地方继续。
  5. 如果yield在循环中使用它,它只会在每次我们在调迭代器上调用next()方法时执行一次。

例1:

下面的代码展示了如何使用generator方法(*getIterator())实现遵循所有规则的next的方法,而不是使用Symbol.iterator方法。

在类中使用generator

例2:

可以进一步简化它。使函数成为generator(带*语法),并使用一次yield返回一个值,如下所示。

直接使用Generators作为函数 重要说明:虽然在上面的例子中,使用“iterator”这个词来表示allUsers ,但它确实是一个generator对象。

generator对象具有方法throw和方法return之外的next方法,但是出于实际目的,我们可以将返回的对象用作“迭代器”。

原因2 - 提供更好和更新的控制流程

帮助提供新的控制流程,帮助我们以新的方式编写程序并解决诸如“回调地狱”之类的问题。

请注意,与普通函数不同,generator函数可以yield(存储函数statereturn值)并准备好在其产生的点处获取其他输入值。

在下面的图片中,每次看到yield它都可以返回值。可以使用generator.next(“some new value”)在它产生的位置使用并传递新值。

正常函数与generator函数

以下示例更具体地说明了控制流如何工作:

generator控制流程

generator语法和用法

generator功能可以通过以下方式使用:

我们可以在“yield”之后获得更多代码(与“return”语句不同)

就像return关键字一样,yield关键字也会返回值 - 但它允许我们在yielding之后拥有代码

可以有多个yield

可以有多个yield语句

通过next方法向generators来回发送值

迭代器next方法还可以将值传递回generator,如下所示。

事实上,这个功能使generator能够消除“回调地狱”。稍后将了解更多相关信息。

此功能也在redux-saga等库中大量使用。

在下面的示例中,我们使用空next()调用来调用迭代器。然后,当我们第二次调用时传递23作为参数next(23)

通过next从外部将值传回generator

generator帮助消除“回调地狱”

如果有多个异步调用,会进入回调地狱。

下面的示例显示了诸如“co”之类的库如何使用generator功能,该功能允许我们通过该next方法传递值以帮助我们同步编写异步代码。

注意co函数如何通过next(result)步骤5和步骤10 将结果从promise发送回generator。

制流程像“co”这样使用“next(<someval>)”lib的逐步解释

好的,让我们继续async / await

异步/ AWAIT

为什么?

正如之前看到的,Generators可以帮助消除“回调地狱”,但需要一些第三方库co来实现这一点。但是“回调地狱”是一个很大的问题,ECMAScript委员会决定为Generator创建一个包装器并推出新的关键字async/await

GeneratorsAsync / Await之间的区别是:

  1. async / await使用await而不是yield
  2. await仅适用于Promises
  3. Async / Await使用async function关键字,而不是function*

所以async/await基本上是Generators的一个子集,并且有一个新的语法糖。

async关键字告诉JavaScript编译器以不同方式处理该函数。只要到达await函数中的关键字,编译器就会暂停。它假定表达式await返回一个promise并等待,直到promise被解决或拒绝,然后才进一步移动。

在下面的示例中,getAmount函数正在调用两个异步函数getUsergetBankBalance 。我们可以在promise中做到这一点,但使用async await更优雅和简单。

ASYNC ITERATORS

为什么?

这是一个非常常见的场景,我们需要在循环中调用异步函数。因此,在ES2018(已完成的提案)中,TC39委员会提出了一个新的Symbol Symbol.asyncIterator和一个新的构造,for-await-of以帮助我们轻松地循环异步函数。

常规Iterator对象和异步迭代器之间的主要区别如下:

Iterator对象

  1. Iterator对象的next()方法返回值如{value: ‘some val’, done: false}
  2. 用法: iterator.next() //{value: ‘some val’, done: false}

Async Iterator对象

  1. Async Iterator对象的next()方法返回一个Promise,后来解析成类似的{value: ‘some val’, done: false}
  2. 用法: iterator.next().then(({ value, done })=> {//{value: ‘some val’, done: false}} 以下示例显示了for-await-of工作原理以及如何使用它。
    for-await-of(ES2018)

总结

Symbol - 提供全局唯一的数据类型。主要使用它们作为对象属性来添加新行为,因此不会破坏像Object.keys和for-in循环这样的标准方法。

众所周知的Symbols- 由JavaScript自动生成的Symbols,可用于在我们的自定义对象中实现核心方法

Iterables- 是存储数据集合并遵循特定规则的任何对象,以便我们可以使用标准for-of循环和...扩展运算符从中提取数据。

Iterators- 由Iterables返回并具有next方法它实际上是从Iterables中提取数据。

Generator -为Iterables提供更高级别的抽象。它们还提供了新的控制流,可以解决诸如回调地狱之类的问题,并为诸如此类的事物提供构建块Async/Await。

Async/Await- 为generator提供更高级别的抽象,以便专门解决回调地狱问题。

Async迭代器- 一种全新的2018功能,可帮助循环异步函数数组,以获得每个异步函数的结果,就像在普通循环中一样。

进一步阅读

ECMAScript 2015+

  1. 以下是ECMAScript 2016,2017和2018中所有新功能的示例
  2. 查看这些有用的ECMAScript 2015(ES6)提示和技巧
  3. 5个在ES6中修复的JavaScript“坏”部分
  4. ES6中的“类”是新的“坏”部分吗?