某些 JavaScript(ECMAScript)特性比其他的容易理解。生成器(Generators)看起來很奇怪——像 C/C++ 中的指針。類型(Symbols)看起來同時既像原語又像對象。
這些特性都是相互關聯,相互構建的。因此你不能脫離其他特性而只理解一個。
因此在本文,我會涉及到類型、全局類型、迭代器、可迭代對象、生成器、異步/等待,以及異步迭代器。首先我會解釋“為什麼”他們在這裡,然後我會用一些有用的例子來展示他們是如何工作的。
這是一個相當高階的問題,但是它並不複雜。本文會讓你牢牢掌握這些所有概念。好的,我們開始吧。
Symbols
在 ES2015,一個名為 symbol 的新的(第六類)數據類型產生了。
為什麼?
這裡列出三個主要原因:
原因 1 ——添加向後兼容的新的內核特性
JavaScript 開發者和 ECMAScript 委員會(TC39)需要一種可以添加新的對象屬性的方式,而不打破已有的方法,比如循環中的 for 或者 Object.keys 。
例如,如果我有一個對象,var myObject = {firstName:'raja', lastName:'rao'} ,如果我運行 Object.keys(myObject) ,會返回 [firstName, lastName] 。
現在如果我添加了另一個屬性,也就是在 myObject 添加 newProperty ,如果你運行 Object.keys(myObject) ,那麼應該仍然返回之前的值,[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 允許鉤子(hooks)調用到內核方法
想象你希望使用一些內核函數,比如說 String.prototype.search 來調用你的自定義函數。也就是說, ‘somestring’.search(myObject); 應該調用 myObject 的搜索函數,並將 ‘somestring’ 作為參數傳入!怎樣才能做到?
這就是 ES2015 提出的一系列全局 symbols ,即被稱為“眾所周知” 的 symbols 。而且只要你的對象包含這些 symbols 的其中一個作為屬性,你就能將內核函數重新定位來調用你的函數!
關於這部分,在此我們不多說,我會在本文後面部分深入討論。但是首先,我們先了解 Symbols 實際上是怎麼工作的。
創建 Symbols
你可以通過調用名為 Symbol 全局的函數/對象創建一個 symbol 。這個函數返回了一個數據類型為 symbol 的值。
注:因為 Symbol 有方法,它們表面上可能與對象相似,但他們不是——他們是原語。你可以將它們看做一個“特殊”對象,他們與一般的對象有相似之處,但是他們表現的不像對象。
例如:Symbols 和對象一樣有方法,但是不同於對象,它們是不可變的且唯一的。
Symbols 不能使用 “new” 關鍵字來創建
因為 symbols 不是對象,而 new 關鍵字返回了一個對象,我們不能使用 new 返回一個 symbols 數據類型。
var mySymbol = new Symbol(); //throws error
Symbols 有“描述”
Symbols 可以包含一個描述——就是為了記錄日誌而使用。
//mySymbol variable now holds a "symbol" unique value
//its description is "some text"
const mySymbol = Symbol('some text');
Symbols 具有唯一性
const mySymbol1 = Symbol('some text');
const mySymbol2 = Symbol('some text');
mySymbol1 == mySymbol2 // false
如果我們使用 “Symbol.for” 方法,Symbols 表現的像單例模式
如果不通過 Symbol() 創建 Symbol ,你可以調用 Symbol.for(
var mySymbol1 = Symbol.for('some key'); //creates a new symbol
var mySymbol2 = Symbol.for('some key'); // **returns the same symbol
mySymbol1 == mySymbol2 //true
使用 .for 的實際運用就是在一個地方創建一個 Symbol ,然後在其他地方訪問相同的 Symbol 。
警告:Symbol.for 會使 symbol 不具有唯一性,因此如果 key 相同,你最後會重寫裡面的值。如果可能的話,儘量避免這麼做!
Symbol 的“描述” vs. “key”
若只是為了更清楚的說,如果你不使用 Symbol.for ,那麼 Symbols 是具有唯一性的。然而,如果你使用了它,而且如果你的 key 不是唯一的,那麼返回的 symbols 也不是唯一的。
Symbols 可以是一個對象屬性鍵
這是 Symbols 的一個非常奇特的事情——而且也是最令人困惑的。儘管他們看起來像一個對象,他們確實是原語。我們可以將 symbol 像 String 一樣作為一個屬性鍵關聯到一個對象。
事實上,這也是使用 Symbols 的主要方式——作為對象屬性!
注:使用 symbols 的對象屬性稱為“鍵屬性”。
括號操作符 vs. 點操作符
因為點操作符只能用於字符串屬性,在這你不能使用點操作符,因此你應該使用括號操作符。
使用 Symbols 的三個主要原因——回顧
現在我們回顧一下(上面說到的)三個主要原因來了解 Symbols 是如何工作的。
原因 #1 ——對於循環和其他的方法來說, Symbols 是不可見的
下面例子中的 for-in 循環遍歷了對象 obj ,但是不知道(或者忽略了)prop3 和 prop4 ,因為它們是 symbols 。
下面是另一個例子, Object.keys 和 Object.getOwnPropertyNames 方法忽略了 Symbols 的特性名稱。
原因 #2 ——Symbol 是唯一的
假設你想要一個叫做 Array.prototype.includes 的全局 Array 對象。它將與 JavaScript(ES2018)開箱即用的 includes 方法衝突。你該如何添加它才能不衝突呢?
首先,用 Symbol 創建一個名為 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 這樣的核心對象實現了 Symbol.search 的 Symbol ,確實做了這個工作。
迭代器和可迭代對象
為什麼?
在我們大多數的 app 中,我們在不斷的處理數據列表,然後需要將這些數據展示到瀏覽器上或者手機 app 中。我們一般會寫我們自己的方法去存儲和取出數據。
但事實是,我們已經有了像 for-of 一樣的循環和展開標識符(…),可以從像數組,字符串,和 map 這樣的標準對象取出數據集合。為什麼我們不能在我們的對象中也用這些標準方法?
在下面的例子,我們不能使用 for-of 循環或者展開標識符來從 Users 類中獲取數據。我們必須用一個自定義的 get 方法。
但是,在我們自己的對象中可以使用這些現有方法不是更好嗎?為了完成這個想法,我們需要有一些規則讓所有的開發者都可以遵循並可以讓他們的對象也使用這些現有的方法。
如果他們遵循這些規則來從他們的對象中取出數據,那麼這些對象就稱為“可迭代對象(iterables)”。
規則如下:
主對象/類應該存儲一些數據。
主對象/類必須有全局的“眾所周知的” symbol ,即 symbol.iterator 作為它的屬性,然後按照從規則 #3 到 #6 的每條規則來實現一個特有的方法。
symbol.iterator 方法必須返回另一個對象 —— 一個“迭代器”對象。
“迭代器”對象必須有一個名為 next 的方法。
next 方法應該可以訪問存儲在規則 #1 的數據。
如果我們調用 iteratorObj.next() ,應該返回存儲在規則 #1 中的數據,如果想返回更多的值使用格式 {value:<stored>, done: false} ,如果不想返回其他更多的值則使用格式 {done: true} 。/<stored>
如果循序了所有的這 6 條規則,規則 #1 中的主對象就被稱為“可迭代對象”。它返回的對象稱為“迭代器”。
我們來看一下我們如何讓我們的 Users 對象作為可迭代對象:
重要提示:如果我們調用一個可迭代對象(allUsers)for-off 循環或者展開標識符,他們內部調用 <iterable>[Symbol.iterator]() 來獲取迭代器(就像 allUsersIterator ),然後使用迭代器來取出數據。/<iterable>
因此在某種程度上,所有這些規則都有一種標準方法來返回一個 iterator 對象。
生成器函數
為什麼?
兩個主要原因如下:
提供可迭代對象的高級抽象
提供新的流程控制來改善“回調地獄”之類的情況
下面我們一一說明。
理由 1 ——可迭代對象的包裝
為了使我們的類/對象編程一個可迭代對象 ,除了通過遵循所有這些規則,我們還可以通過簡單地創建一些稱為“生成器”的函數來簡化這些操作。
關於生成器的一些要點如下:
生成器函數在類中有一個新的 *<mygenerator> 語法,而且生成器函數有語法 function * myGenerator(){} 。/<mygenerator>
調用生成器 myGenerator() 返回一個生成器對象,它也實現了迭代器協議(規則),因此我們可以使用它作為一個可以直接使用的迭代器返回值。
-
生成器使用一個特有的 yield 聲明來返回數據。
yield 聲明保持記錄前一個調用,而且可以簡單的從它停止的地方繼續。
如果你在一個循環中使用 yield ,每次我們在迭代器調用 next() 方法的時候,它會只運行一次。
示例一:
下面的代碼可以向你展示你如何使用給一個生成器函數 (*getIterator()) 來代替使用 Symbol 。按照所有的規則的 iterator 方法和實現 next 方法。
示例二:
你可以使其更加簡化。創建一個函數為生成器(使用*語法),同時如下所示使用 yield 一次返回一個返回值。
重要提示:儘管上面的例子中,我使用了單詞 “iterator” 來表示 allUsers ,但是它確實是一個生成器對象。
生成器對象出來有 next 方法之外還有 throw 和 return 之類的方法!但是實際上,我們可以使用返回的對象,就像“迭代器”一樣。
理由 2 ——提供更好更新的流程控制
提供新的流程控制可以幫助我們使用新的方式編寫程序,也可以解決像“回調地獄”之類的問題。
注意生成器函數不像一個普通的函數,它可以 yield (存儲函數的狀態和返回值),而且在它 yielded 的時候就會準備著去獲取額外的輸入值。
在下面的圖片中,每當它看見 yield ,就會返回這個值。你可以使用 generator.next(“一些新值”),而且在 yielded 時將新值傳遞出去。
下面的例子更加具體地展示了流程控制如何工作:
生成器語法和用法
生成器函數可以通過下面的方法調用:
我們可以在 “yield” 之後寫更多的代碼(不像 “return” 語句)
就像 return 關鍵字,yield 關鍵字也會返回值——但是它允許在 yielding 之後還有代碼!
你可以有多個 yields
通過 “next” 方法來回給生成器傳值
迭代器的 next 方法也可以將值傳遞給生成器,就像下面寫的。
事實上,這個特性可以讓生成器消除“回調地獄”。在這方面你會了解的更多一點。
這個特性也在庫中大量使用,例如redux-saga。
下面的例子中,我們使用一個空的 next() 調用迭代器來得到問題。然後,當我們第二次調用 next(23) 時我們傳入 23 作為值。
生成器有助於消除“回調地獄”。’如果我們有多個異步調用的時候,你知道的我們會進入回調地獄。
下面的例子就展示了像“co”之類的庫如何利用生成器特性,讓我們通過 next 方法傳值來幫助我們同步地寫異步代碼。
注意在第5步和第10步,co 函數如何通過 next(result) 將結果從 promise 傳回給生成器。
好的,我們來討論異步/等待。
異步/等待
為什麼?
就像你之前看到的,生成器有助於消除“回調地獄”,但是你需要一些像 co 一樣的第三方的庫才能完成。但是“回調地獄”仍然是一個大問題,ECMAScript 委員會決定只為生成器的這個問題創建一個封裝,同時提出了新的關鍵字 async/await 。
生成器和異步/等待的區別如下:
異步/等待使用 await 而不是 yield 。
await 只對 Promises 有用。
它使用 async 函數關鍵字而不是 function* 。
因此異步/等待是生成器的一個重要子集,它包含了一個新的語法糖(Syntactic sugar)。
async 關鍵字告訴 JavaScript 編譯器對這種函數區別對待。在函數中無論何時編譯器只要遇到 await 關鍵字,編譯器都會暫停。假定 await 後面的表達式返回了一個 promise ,而且在程序繼續向下走之前一直等 promise 被處理或者被拒絕。
在下面的例子中,getAmount 函數調用了兩個異步函數 getUser 和 getBankBalance 。我們在一個 promise 中也可以這麼做,但是使用 async await 更加優雅且簡單。
異步迭代器
為什麼?
我們在循環中需要調用異步函數的情況非常常見。因此在 ES2018(已完成提案),TC39 委員會提出了新的 Symbol 的 Symbol.asyncIterator ,以及一個新的 for-await-of 結構來幫助我們簡化對異步函數循環。
普通迭代器對象和異步迭代器對象的主要區別如下:
迭代器對象
迭代器對象的 next() 方法返回了像 {value: ‘some val’, done: false} 這樣的值
用法:iterator.next() //{value: ‘some val’, done: false}
異步迭代器對象
異步迭代器對象的 next() 方法返回了一個 Promise ,解析後為 {value: ‘some val’, done: false} 這樣的內容
用法:iterator.next().then(({ value, done })=> {//{value: ‘some val’, done: false}}
下面的例子展示了 for-await-of 是如何工作的,以及你怎麼才能使用它。
閱讀更多 IT技術之家 的文章
關鍵字: JavaScript C語言 迭代