前端人不了解的迭代器和生成器
迭代器和生成器是 ES6 中引入的特性。迭代器通過一次消費一個項目列表來提高效率,類似于數據流。生成器是一種能夠暫停執行的特殊函數。調用生成器允許以塊的形式(一次一個)生成數據,而無需先將其存儲在列表中。下面就來深入理解 JavaScript 中的迭代器和生成器,看看它們是如何使用的,又有何妙用!
迭代器
JavaScript 中的迭代器可以分別兩種:同步迭代器和異步迭代器。
1. 同步迭代器
(1)迭代器和可迭代對象
在 JavaScript 中有很多方法可以遍歷數據結構。例如,使用 for 循環或使用 while 循環。迭代器具有類似的功能,但有顯著差異。
迭代器只需要知道集合中的當前位置,而其他循環則需要預先加載整個集合才能循環遍歷它。迭代器使用 next() 方法訪問集合中的下一個元素。但是,為了使用迭代器,值或數據結構應該是可迭代的。數組、字符串、映射、集合是 JavaScript 中的可迭代對象。普通對象是不可迭代的。
(2)定義迭代器
下面來看看集合不可迭代的場景:
const favouriteMovies = {
a: '哈利波特',
b: '指環王',
c: '尖峰時刻',
d: '星際穿越',
e: '速度與激情',
}
這個對象是不可迭代的。如果使用普通的 for 循環遍歷它,就會拋出錯誤。隨著 ES6 中迭代器的引入,可以將其轉換為可迭代對象以便遍歷它。這些稱為自定義迭代器。下面看看如何實現對象的遍歷并打印出來:
favouriteMovies[Symbol.iterator] = function() {
const ordered = Object.values(this).sort((a, b) => a - b);
let i = 0;
return {
next: () => ({
done: i >= ordered.length,
value: ordered[i++]
})
}
}
for (const v of favouriteMovies) {
console.log(v);
}
輸出結果如下:
哈利波特
指環王
尖峰時刻
星際穿越
速度與激情
這里使用 Symbol.iterator() 來定義迭代器。任何具有 Symbol.iterator 鍵的結構都是可迭代的。
可迭代對象具有以下行為:
- 當 for..of 循環開始時,它首先查找錯誤。如果未找到,則它會訪問方法和定義該方法的對象。
- 以 for..of 循環方式迭代該對象。
- 使用該輸出對象的 next() 方法來獲取要返回的下一個值。
- 返回的值的格式為 done:boolean, value: any。返回 done:true 時循環結束。
下面來創建一個 LeapYear 對象,該對象返回范圍為 (start, end) 的閏年列表,并在后續閏年之間設置間隔。
class LeapYear {
constructor(start = 2020, end = 2040, interval = 4) {
this.start = start;
this.end = end;
this.interval = interval;
}
[Symbol.iterator]() {
let nextLeapYear = this.start;
return {
next: () => {
if (nextLeapYear <= this.end) {
let result = { value: nextLeapYear, done: false };
nextLeapYear += this.interval;
return result;
}
return { value: undefined, done: true };
},
};
}
}
在上面的代碼中,為自定義類型 LeapYear 實現了 Symbol.iterator() 方法。分別在 this.start 和 this.end 字段中有迭代的起點和終點。使用 this.interval來跟蹤迭代的第一個元素和下一個元素之間的間隔。
現在,可以在自定義類型上調用 for...of 循環,并查看其行為和輸出值,就像默認數組類型一樣:
let leapYears = new LeapYear();
for (const leapYear of leapYears) {
console.log(leapYear);
}
輸出結果如下:
2020
2024
2028
2032
2036
2040
這里的 LeapYear 通過 Symbol.iterator() 變成了可迭代對象。
在一些情況下,迭代器會比普通迭代更好。例如,在沒有隨機訪問的有序集合(如數組)中,迭代器的性能會更好,因為它可以直接根據當前位置檢索元素。但是,對于無序集合,由于沒有順序,就不會體驗到性能上的重大差異。
使用普通循環算法,例如 for 循環或 while 循環,您只能循環遍歷允許迭代的集合:
const favourtieMovies = [
'哈利波特',
'指環王',
'尖峰時刻',
'星際穿越',
'速度與激情',
];
for (let i=0; i < favourtieMovies.length; i++) {
console.log(favouriteMovies[i]);
}
let i = 0;
while (i < favourtieMovies.length) {
console.log(favourtieMovies[i]);
i++;
}
由于數組是可迭代的,因此可以使用 for 循環遍歷。我們也可以為上面實現一個迭代器,這將允許更好地訪問基于當前位置的元素,而無需加載整個集合。代碼如下:
const iterator = favourtieMovies[Symbol.iterator]();
iterator.next(); // { value: '哈利波特', done: false }
iterator.next(); // { value: '指環王', done: false }
iterator.next(); // { value: '尖峰時刻', done: false }
iterator.next(); // { value: '星際穿越', done: false }
iterator.next(); // { value: '速度與激情', done: false }
iterator.next(); // { value: undefined, done: true }
next() 方法將返回迭代器的結果。它包括兩個值;集合中的元素和完成狀態。可以看到,當遍歷完成后,即使訪問數組外的元素,也不會拋出錯誤。它只會返回一個具有 undefined 值和完成狀態為 true 的對象。
(3)使用場景
那為什么向自定義對象中添加迭代器呢?我們也可以編寫自定義函數來遍歷對象以完成同樣的事情。
實際上,迭代器是一種標準化自定義對象的優雅實現方式,它為自定義數據結構提供了一種在更大的 JS 環境中很好地工作的方法。因此,提供自定義數據結構的庫經常會使用迭代器。例如, Immutable.JS 庫就使用迭代器為其自定義對象(如Map)。所以,如果需要為封裝良好的自定義數據結構提供原生迭代功能,就考慮使用迭代器。
2. 異步迭代器
JavaScript 中的異步迭代對象是實現 Symbol.asyncIterator 的對象:
const asyncIterable = {
[Symbol.asyncIterator]: function() {
}
};
我們可以將一個函數分配給 [Symbol.asyncIterator] 以返回一個迭代器對象。迭代器對象應符合帶有 next() 方法的迭代器協議(類似于同步迭代器)。
下面來添加迭代器:
const asyncIterable = {
[Symbol.asyncIterator]: function() {
let count = 0;
return {
next() {
count++;
if (count <= 3) {
return Promise.resolve({ value: count, done: false });
}
return Promise.resolve({ value: count, done: true });
}
};
}
};
這里用 Promise.resolve 包裝了返回的對象。下面來執行 next() 方法:
const go = asyncIterable[Symbol.asyncIterator]();
go.next().then(iterator => console.log(iterator.value));
go.next().then(iterator => console.log(iterator.value));
輸出結果如下:
1
2
也可以使用 for await...of 來對異步迭代對象進行迭代:
async function consumer() {
for await (const asyncIterableElement of asyncIterable) {
console.log(asyncIterableElement);
}
}
consumer();
異步迭代器和迭代器是異步生成器的基礎,后面會介紹異步生成器。
生成器
JavaScript 中的生成器可以分別兩種:同步生成器和異步生成器。
1. 同步生成器
(1)基本概念
生成器是一個可以暫停和恢復并可以產生多個值的過程。JavaScript 中的生成器由一個生成器函數組成,它返回一個可迭代 Generator 對象。
生成器是對 JavaScript 的強大補充。它們可以維護狀態,提供一種制作迭代器的有效方法,并且能夠處理無限數據流,可用于在前端實現無限滾動等。此外,當與 Promises 一起使用時,生成器可以模擬 async/await 功能,這使我們能夠以更直接和可讀的方式處理異步代碼。盡管 async/await 是處理常見、簡單的異步用例(例如從 API 獲取數據)的一種更普遍的方式,但生成器具有更高級的功能。
生成器函數是返回生成器對象的函數,由 function 關鍵字后面跟星號 (*) 定義,如下所示:
function* generatorFunction() {}
有時,我們可能會在函數名稱旁邊看到星號,而不是 function 關鍵字,例如 function *generatorFunction(),它的工作原理是相同的,但 function* 是一種更廣泛接受的語法。
生成器函數也可以在表達式中定義,就像常規函數一樣:
const generatorFunction = function* () {}
生成器甚至可以是對象或類的方法:
// 生成器作為對象的方法
const generatorObj = {
*generatorMethod() {},
}
// 生成器作為類的方法
class GeneratorClass {
*generatorMethod() {}
}
下面的例子都將使用生成器函數聲明得語法。
注意:與常規函數不同,生成器不能使用 new 關鍵字構造,也不能與箭頭函數結合使用。
現在我們知道了如何聲明生成器函數,下面來看看生成器返回的可迭代生成器對象。
(2)生成器對象
傳統的 JavaScript 函數會在遇到return 關鍵字時返回一個值。如果省略 return 關鍵字,函數將隱式返回 undefined。
例如,在下面的代碼中,我們聲明了一個 sum() 函數,它返回一個值,該值是兩個整數參數的和:
function sum(a, b) {
return a + b
}
調用該函數會返回一個值,該值是參數的總和:
const value = sum(5, 6) // 11
而生成器函數不會立即返回值,而是返回一個可迭代的生成器對象。在下面的例子中,我們聲明了一個函數并給它一個單一的返回值,就像一個標準的函數:
function* generatorFunction() {
return 'Hello, Generator!'
}
當調用生成器函數時,它將返回生成器對象,我們可以將其分配給一個變量:
const generator = generatorFunction()
如果這是一個常規函數,我們希望生成器為我們提供函數中返回的字符串。然而,我們實際得到的是一個處于掛起狀態的對象。因此,調用生成器將提供類似于以下內容的輸出:
generatorFunction {<suspended>}
[[GeneratorLocation]]: VM335:1
[[Prototype]]: Generator
[[GeneratorState]]: "suspended"
[[GeneratorFunction]]: ?* generatorFunction()
[[GeneratorReceiver]]: Window
函數返回的生成器對象是一個迭代器。迭代器是一個具有可用的 next() 方法的對象,該方法用于迭代一系列值。next() 方法返回一個對象,其包含兩個屬性:
- value:當前步驟的值;
- done:布爾值,指示生成器中是否有更多值。
next() 方法必須遵循以下規則:
- 返回一個帶有 done: false 的對象來繼續迭代;
- 返回一個帶有 done: true 的對象來停止迭代。
下面就來在生成器上調用 next() 并獲取迭代器的當前值和狀態:
generator.next()
這將得到以下輸出結果:
{value: "Hello, Generator!", done: true}
調用 next() 時的返回值為 Hello, Generator!,并且 done 的狀態為 true,因為該值來自關閉迭代器的返回值。由于迭代器完成,生成器函數的狀態將從掛起變為關閉。這時再次調用生成器將輸出以下內容:
generatorFunction {<closed>}
除此之外,生成器函數也有區別于普通函數的獨特特征。下面我們就來了解一下 yield 運算符并看看生成器如何暫停和恢復執行。
(3)yield 運算符
生成器為 JavaScript 引入了一個新的關鍵字:yield。**yield**** 可以暫停生成器函數并返回 **yield** 之后的值,從而提供一種輕量級的方法來遍歷值。**
在下面的例子中,我們將使用不同的值暫停生成器函數三次,并在最后返回一個值。然后將生成器對象分配給 generator 變量。
function* generatorFunction() {
yield 'One'
yield 'Two'
yield 'Three'
return 'Hello, Generator!'
}
const generator = generatorFunction()
現在,當我們在生成器函數上調用 next() 時,它會在每次遇到 yield 時暫停。done 會在每次 yield 后設置為 false,表示生成器還沒有結束。一旦遇到 return,或者函數中沒有更多的 yield 時,done 就會變為 true,生成器函數就結束了。
連續四次調用 next() 方法:
generator.next()
generator.next()
generator.next()
generator.next()
這些將按順序得到以下結果:
{value: "One", done: false}
{value: "Two", done: false}
{value: "Three", done: false}
{value: "Hello, Generator!", done: true}
next() 非常適合從迭代器對象中提取有限數據。
注意,生成器不需要 return;如果省略,最后一次迭代將返回 {value: undefined, done: true},生成器完成后對 next() 的任何后續調用也是如此。
(4)遍歷生成器
使用 next() 方法可以遍歷生成器對象,接收完整對象的所有 value 和 done 屬性。不過,就像 Array、Map 和 Set 一樣,Generator 遵循迭代協議,并且可以使用 for...of 進行迭代:
function* generatorFunction() {
yield 'One'
yield 'Two'
yield 'Three'
return 'Hello, Generator!'
}
const generator = generatorFunction()
for (const value of generator) {
console.log(value)
}
輸出結果如下:
One
Two
Three
擴展運算符也可用于將生成器的值分配給數組:
const values = [...generator]
console.log(values)
輸出結果如下:
['One', 'Two', 'Three']
可以看到,擴展運算符和 for...of 都不會將 return 的值計入 value。
注意:雖然這兩種方法對于有限生成器都是有效的,但如果生成器正在處理無限數據流,則無法在不創建無限循環的情況下直接使用擴展運算符或 for...of。
我們還可以從迭代結果中解構值:
const [a, b, c]= generator;
console.log(a);
console.log(b);
console.log(c);
輸出結果如下:
One
Two
Three
(5)關閉生成器
如我們所見,生成器可以通過遍歷其所有值將其 done 屬性設置為 true 并將其狀態設置為 closed 。除此之外,還有兩種方法可以立即關閉生成器:使用 return() 方法和使用 throw() 方法。
使用 return(),生成器可以在任何時候終止,就像在函數體中的 return 語句一樣。可以將參數傳遞給 return(),或將其留空以表示未定義的值。
下面來創建一個具有 yield 值但在函數定義中沒有 return 的生成器:
function* generatorFunction() {
yield 'One'
yield 'Two'
yield 'Three'
}
const generator = generatorFunction()
第一個 next() 將返回“One”,并將 done 設置為 false。如果在那之后立即在生成器對象上調用 return() 方法,將獲得傳遞的值并將 done 設置為 true。對 next() 的任何額外調用都會給出默認的已完成生成器響應,其中包含一個 undefined 值。
generator.next()
generator.return('Return!')
generator.next()
輸出結果如下:
{value: "Neo", done: false}
{value: "Return!", done: true}
{value: undefined, done: true}
return() 方法會強制生成器對象完成并忽略任何其他 yield 關鍵字。當需要使函數可取消時,這在異步編程中特別有用,例如當用戶想要執行不同的操作時中斷數據請求,因為無法直接取消 Promise。
如果生成器函數的主體有捕獲和處理錯誤的方法,則可以使用 throw() 方法將錯誤拋出到生成器中。這將啟動生成器,拋出錯誤并終止生成器。
下面來在生成器函數體內放一個 try...catch 并在發現錯誤時記錄錯誤:
function* generatorFunction() {
try {
yield 'One'
yield 'Two'
} catch (error) {
console.log(error)
}
}
const generator = generatorFunction()
現在來運行 next() 方法,然后運行 throw() 方法:
generator.next()
generator.throw(new Error('Error!'))
輸出結果如下:
{value: "One", done: false}
Error: Error!
{value: undefined, done: true}
使用 throw() 可以將錯誤注入到生成器中,該錯誤被 try...catch 捕獲并記錄到控制臺。
(6)生成器對象方法和狀態
下面是生成器對象的方法:
- next():返回生成器中的后面的值;
- return():在生成器中返回一個值并結束生成器;
- throw():拋出錯誤并結束生成器。
下面是生成器對象的狀態:
- suspended:生成器已停止執行但尚未終止。
- closed:生成器因遇到錯誤、返回或遍歷所有值而終止。
(7)yield 委托
除了常規的 yield 運算符之外,生成器還可以使用 yield* 表達式將更多值委托給另一個生成器。當在生成器中遇到 yield* 時,它將進入委托生成器并開始遍歷所有 yield 直到該生成器關閉。這可以用于分離不同的生成器函數以在語義上組織代碼,同時仍然讓它們的所有 **yield** 都可以按正確的順序迭代。
下面來創建兩個生成器函數,其中一個將對另一個進行 yield* 操作:
function* delegate() {
yield 3
yield 4
}
function* begin() {
yield 1
yield 2
yield* delegate()
}
接下來,遍歷 begin() 生成器函數:
const generator = begin()
for (const value of generator) {
console.log(value)
}
輸出結果如下:
1
2
3
4
外部的生成器(begin)生成值 1 和 2,然后使用 yield* 委托給另一個生成器(delegate),返回 3 和 4。
yield* 還可以委托給任何可迭代的對象,例如 Array 或 Map。yield 委托有助于組織代碼,因為生成器中任何想要使用 yield 的函數也必須是一個生成器。
(8)在生成器中傳遞值
上面的例子中,我們使用生成器作為迭代器,并且在每次迭代中產生值。除了產生值之外,生成器還可以使用 next() 中的值。在這種情況下,yield 將包含一個值。
需要注意,調用的第一個 next() 不會傳遞值,而只會啟動生成器。為了證明這一點,可以記錄 yield 的值并使用一些值調用 next() 幾次。
function* generatorFunction() {
console.log(yield)
console.log(yield)
return 'End'
}
const generator = generatorFunction()
generator.next()
generator.next(100)
generator.next(200)
輸出結果如下:
100
200
{value: "End", done: true}
除此之外,也可以為生成器提供初始值。下面來創建一個 for 循環并將每個值傳遞給 next() 方法,同時將一個參數傳遞給 inital 函數:
function* generatorFunction(value) {
while (true) {
value = yield value * 10
}
}
const generator = generatorFunction(0)
for (let i = 0; i < 5; i++) {
console.log(generator.next(i).value)
}
這將從 next() 中檢索值并為下一次迭代生成一個新值,該值是前一個值乘以 10。輸出結果如下:
0
10
20
30
40
處理啟動生成器的另一種方法是將生成器包裝在一個函數中,該函數將會在執行任何其他操作之前調用 next() 一次。
(9)async/await
async/await 使處理異步數據更簡單、更容易理解。生成器具有比異步函數更廣泛的功能,但能夠復制類似的行為。以這種方式實現異步編程可以增加代碼的靈活性。
下面來構建一個異步函數,它使用 Fetch API 獲取數據并將響應記錄到控制臺。
首先定義一個名為 getUsers 的異步函數,該函數從 API 獲取數據并返回一個對象數組,然后調用 getUsers:
const getUsers = async function () {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const json = await response.json()
return json
}
getUsers().then((response) => console.log(response))
輸出結果如下:
圖片
使用生成器可以創建幾乎相同但不使用 async/await 關鍵字的效果。相反,它將使用我們創建的新函數,并產生值而不是等待 Promise。
const getUsers = asyncAlt(function* () {
const response = yield fetch('https://jsonplaceholder.typicode.com/users')
const json = yield response.json()
return json
})
getUsers().then((response) => console.log(response))
如我們所見,它看起來與 async/await 實現幾乎相同,除了有一個生成器函數被傳入以產生值。
現在可以創建一個類似于異步函數的 asyncAlt 函數。asyncAlt 有一個 generatorFunction 參數,它是產生 fetch 返回的 Promise 的函數。asyncAlt 返回函數本身,并 resolve 它得到的每個 Promise,直到最后一個:
function asyncAlt(generatorFunction) {
return function () {
// 創建并分配生成器對象
const generator = generatorFunction()
// 定義一個接受生成器下一次迭代的函數
function resolve(next) {
// 如果生成器關閉并且沒有更多的值可以生成,則解析最后一個值
if (next.done) {
return Promise.resolve(next.value)
}
// 如果仍有值可以產生,那么它們就是Promise,必須 resolved。
return Promise.resolve(next.value).then((response) => {
return resolve(generator.next(response))
})
}
// 開始 resolve Promise
return resolve(generator.next())
}
}
這樣就會得到和async/await一樣的結果:
圖片
盡管這個方法可以為代碼增加靈活性,但通常 async/await 是更好的選擇,因為它抽象了實現細節并讓開發者專注于編寫高效代碼。
(10)使用場景
很多開發人員認為生成器函數視為一種奇特的 JavaScript 功能,在現實中幾乎沒有應用。在大多數情況下,確實用不到生成器。
生成器的優點:
- 惰性求值:除非需要,否則不計算值。它提供按需計算。只有需要它時,value 才會存在。
- 內存效率高:由于惰性求值,生成器的內存效率非常高,因為它不會為預先生成的未使用值分配不必要的內存位置。
- 更簡潔的代碼:生成器提供更簡潔的代碼,尤其是在異步行為中。
生成器在對性能要求高的場景中有很大的用處。特別是,它們適用于以下場景:
- 處理大文件和數據集。
- 生成無限的數據序列。
- 按需計算昂貴的邏輯。
Redux sagas 就是實踐中使用的生成器的一個很好的例子。它是一個用于管理redux應用異步操作的中間件,redux-saga 通過創建 sagas 將所有異步操作邏輯收集在一個地方集中處理,可以用來代替 redux-thunk 中間件。
2. 異步生成器
ECMAScript 2018 中引入了異步生成器的概念,它是一種特殊類型的異步函數,可以隨意停止和恢復其執行。
同步生成器函數和異步生成器函數的區別在于,后者從迭代器對象返回一個異步的、基于 Promise 的結果。
要想創建異步生成器函數,需要聲明一個帶有星號 * 的生成器函數,前綴為 async:
async function* asyncGenerator() {
}
一旦進入函數,就可以使用 yield 來暫停執行:
async function* asyncGenerator() {
yield 'One'
yield 'Two'
}
這里 yield 會暫停執行并返回一個迭代器對象給調用者。這個對象既是可迭代對象,又是迭代器。
異步生成器函數不會像常規函數那樣在一步中計算出所有結果。相反,它會逐步提取值。我們可以使用兩種方法從異步生成器解析 Promise:
- 在迭代器對象上調用 next();
- 使用 for await...of 異步迭代。
對于上面的例子,可以這樣做:
async function* asyncGenerator() {
yield 'One';
yield 'Two';
}
const go = asyncGenerator();
go.next().then(iterator => console.log(iterator.value));
go.next().then(iterator => console.log(iterator.value));
輸出結果如下:
'One';
'Two'
另一種方法使用異步迭代 for await...of。要使用異步迭代,需要用 async 函數包裝它:
async function* asyncGenerator() {
yield 'One';
yield 'Two';
}
async function consumer() {
for await (const value of asyncGenerator()) {
console.log(value);
}
}
consumer();
for await...of 非常適合提取非有限數據流。