原文地址:简单解释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具有与对象类似的方法,但与对象不同,它们是不可变的且唯一的。
“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
的主要方式之一 ,作为对象属性。
[]操作符与.操作符
不能使用.操作符,因为.操作符仅适用于字符串属性,因此应使用[]操作符。
使用Symbol的3个主要原因 - review
让我们回顾一下的三个主要原因来了解Symbol是如何工作的。
原因1 - Symbols对于循环和其他方法是不可见的
下面示例中使用for-in
循环遍历一个对象obj
,但它不知道(或忽略)prop3,prop4
因为它们是symbols。
Object.keys
和
Object.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.search
与String.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的内部工作原理
- 解析
‘rajarao’.search(‘rao’);
- 将
“rajarao”
转换为String对象new String(“rajarao”)
- 将
“rao”
转换为RegExp对象new Regexp(“rao”)
- 调用字符串对象
“rajarao”
的方法search
,传递'rao'
对象为参数。 search
方法调用“rao”对象内部方法Symbol.search
(将搜索委托返回“rao”对象)并传递“rajarao”
。像这样:"rao"[Symbol.search]("rajarao")
"rao"[Symbol.search]("rajarao")
返回索引结果4传递给search
函数,最后,search
返回4到我们的代码。
下面的伪代码片段显示了代码内部的工作方式:
不是一定需要通过RegExp。可以传递任何实现Symbol.search并返回任何所需内容的自定义对象。
自定义String.search方法来调用自定义函数
下面的例子展示了我们如何使String.prototype.search
调用自定义Product类的搜索功能 - 多亏了Symbol.search
全局Symbol
。
Symbol.search(CUSTOM BEHAVIOR)的内部工作原理
- 解析
‘barsoap’.search(soapObj)
; - 将
“barsoap”
转换为String对象new String(“barsoap”)
- 由于
soapObj
已经是对象,不要进行任何转换 - 调用“barsoap”字符串对象的
search
方法。 search
方法调用“soapObj”对象内部方法Symbol.search
(它将搜索委托回“soapObj”对象)并传递“barsoap”
作为参数。像这样:soapObj[Symbol.search]("barsoap")
soapObj[Symbol.search]("barsoap")
返回索引结果FOUND
给search
函数,最后,search
返回FOUND
到我们的代码。
好的,让我们转到Iterators。
迭代器和Iterables
为什么?
在几乎所有的应用程序中,我们都在不断处理数据列表,我们需要在浏览器或移动应用程序中显示这些数据。通常我们编写自己的方法来存储和提取数据。
但问题是,我们已经有了for-of
循环和扩展运算符(…)
等标准方法来从标准对象(如数组,字符串和映射)中提取数据集合。为什么我们不能将这些标准方法用于我们的Object?
在下面的示例中,我们不能使用for-of
循环或(…)
运算符来从Users
类中提取数据。我们必须使用自定义get
方法。
但是,能够在我们自己的对象中使用这些现有方法不是很好吗?为了实现这一点,我们需要制定所有开发人员可以遵循的规则,并使其对象与现有方法一起使用。
如果他们遵循这些规则从对象中提取数据,那么这些对象称为“迭代”。
规则是:
- 主对象/类应该存储一些数据。
- 主对象/类必须具有全局“众所周知的”Symbols
symbol.iterator
作为其属性,Symbols根据规则#3至#6实现特定方法。 - 此
symbol.iterator
方法必须返回另一个对象 - “迭代器”对象。 - 这个“迭代器”对象必须有一个称为
next
的方法。 - 该
next
方法应该可以访问存储在规则1中的数据。 - 如果我们调用
iteratorObj.next()
,它应该返回规则#1中的一些存储数据无论是想要返回更多值{value:<stored data>, done: false}
,还是不想返回任何数据{done: true}
。
如果遵循所有这6个规则,则来自规则#1的主要对象被称为 可迭代。 它返回的对象称为迭代器。
我们来看看如何创建Users
对象和迭代:
重要说明:如果我们传递一个iterable(allUsers)for-of
循环或扩展运算符,将会在内部调用<iterable>[Symbol.iterator]()
获取迭代器(如allUsersIterator
),然后使用迭代器提取数据。
所以在某种程度上,所有这些规则都有一个返回iterator
对象的标准方法。
Generator 函数
为什么?
主要有两个原因:
- 为迭代提供更高级别的抽象
- 提供更新的控制流来帮助解决诸如“回调地狱”之类的问题。
我们来看看它的详细内容。
原因1 - 迭代的包装器
不是通过遵循所有这些规则来使我们的类/对象成为一个iterable
,我们可以简单地创建一个“Generator”方法来简化这件事情。
以下是关于Generator的一些要点:
Generator
方法在内部有一个*<myGenerator>
新语法,Generator
函数有语法function * myGenerator(){}
。- 调用generator
myGenerator()
返回一个实现iterator协议(规则)的generator
对象,因此我们可以将其用作iterator
开箱即用的返回值。 - generator使用特殊yield语句来返回数据。
- yield 语句保持以前的调用状态,并从它停止的地方继续。
- 如果yield在循环中使用它,它只会在每次我们在调迭代器上调用next()方法时执行一次。
例1:
下面的代码展示了如何使用generator方法(*getIterator())
实现遵循所有规则的next
的方法,而不是使用Symbol.iterator
方法。
例2:
可以进一步简化它。使函数成为generator(带*语法),并使用一次yield
返回一个值,如下所示。
“iterator”
这个词来表示
allUsers
,但它确实是一个
generator
对象。
generator对象具有方法throw
和方法return
之外的next
方法,但是出于实际目的,我们可以将返回的对象用作“迭代器”。
原因2 - 提供更好和更新的控制流程
帮助提供新的控制流程,帮助我们以新的方式编写程序并解决诸如“回调地狱”之类的问题。
请注意,与普通函数不同,generator函数可以yield
(存储函数state
和return
值)并准备好在其产生的点处获取其他输入值。
在下面的图片中,每次看到yield它都可以返回值。可以使用generator.next(“some new value”)
在它产生的位置使用并传递新值。
以下示例更具体地说明了控制流如何工作:
generator语法和用法
generator功能可以通过以下方式使用:
我们可以在“yield”之后获得更多代码(与“return”语句不同)
就像return
关键字一样,yield
关键字也会返回值 - 但它允许我们在yielding之后拥有代码
可以有多个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
。
Generators
和Async / Await
之间的区别是:
- async / await使用
await
而不是yield
。 await
仅适用于Promises
。Async / Await
使用async function
关键字,而不是function*
。
所以async/await
基本上是Generators的一个子集,并且有一个新的语法糖。
async
关键字告诉JavaScript编译器以不同方式处理该函数。只要到达await
函数中的关键字,编译器就会暂停。它假定表达式await
返回一个promise
并等待,直到promise
被解决或拒绝,然后才进一步移动。
在下面的示例中,getAmount函数正在调用两个异步函数getUser
和getBankBalance
。我们可以在promise中做到这一点,但使用async await
更优雅和简单。
ASYNC ITERATORS
为什么?
这是一个非常常见的场景,我们需要在循环中调用异步函数。因此,在ES2018(已完成的提案)中,TC39委员会提出了一个新的Symbol Symbol.asyncIterator
和一个新的构造,for-await-of
以帮助我们轻松地循环异步函数。
常规Iterator对象和异步迭代器之间的主要区别如下:
Iterator对象
- Iterator对象的
next()
方法返回值如{value: ‘some val’, done: false}
- 用法:
iterator.next() //{value: ‘some val’, done: false}
Async Iterator对象
- Async Iterator对象的
next()
方法返回一个Promise,后来解析成类似的{value: ‘some val’, done: false}
- 用法:
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功能,可帮助循环异步函数数组,以获得每个异步函数的结果,就像在普通循环中一样。