原文地址:簡單解釋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
從外部將值傳回generatorgenerator幫助消除“回撥地獄”
如果有多個非同步呼叫,會進入回撥地獄。
下面的示例顯示了諸如“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功能,可幫助迴圈非同步函式陣列,以獲得每個非同步函式的結果,就像在普通迴圈中一樣。