Iterables和迭代器
ES6引入了一種遍歷資料的新機制:迭代。兩個概念是迭代的核心:
Symbol.iterator
在TypeScript 中為介面表示方式,如下所示:
interface Iterable { [Symbol.iterator]() : Iterator; } interface Iterator { next() : IteratorResult; } interface IteratorResult { value: any; done: boolean; } 複製程式碼
21.1.1 可迭代的物件
下面幾種:
- Arrays
- Strings
- Maps
- Sets
- DOM data structures (work in progress)
字面量物件是不可迭代的,具體下面會有相關介紹。
21.1.2 內部構造使用迭代的
-
通過陣列模式進行解構:
const [ a , b ] = new Set ([ 'a' , 'b' , 'c' ]); 複製程式碼
-
for-of
迴圈:for ( const [ 'a' , 'b' , 'c' ]) { console .log ( x ); } 複製程式碼
-
Array.from()
:const arr = Array .from ( new Set ([ 'a' , 'b' , 'c' ])); 複製程式碼
-
展開運算子(
...
):const arr = [... new Set ([ 'a' , 'b' , 'c' ])]; 複製程式碼
-
Maps 和Sets 的構造器:
const map = new Map ([[ false , 'no' ], [ true , 'yes' ]]); const set = new Set ([ 'a' , 'b' , 'c' ]); 複製程式碼
-
Promise.all()
,Promise.race()
:Promise .all ( iterableOverPromises ).then ( ··· ); Promise .race ( iterableOverPromises ).then ( ··· ); 複製程式碼
-
yield*
:yield * anIterable ; 複製程式碼
21.2 可迭代性
可迭代性的主要概念如下:
- Data consumers(資料消費者):JavaScript具有使用資料的語言結構。例如,
for-of
迴圈遍歷值,而spread操作符(…
)將值插入陣列或函式呼叫中。 - Data sources(資料來源):資料消費者可以從各種資料來源獲取其值。例如,您可能希望迭代陣列的元素、Map中的鍵值條目或字串的字元。
每個消費者都支援所有來源是不切實際的,特別是因為可以建立新的來源(例如通過庫)。 需要一種統一的介面機制,來處理所有不同的資料結構。 因此,ES6引入了 Iterable
。 資料消費者使用它,資料來源實現它:

因為JS中沒有介面,所以遍歷器(Iterator)更像是一種約定。為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署Iterator介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。
- Source(源):如果某個值的方法的鍵是符號
Symbol.iterator
,它返回一個所謂的迭代器(iterator),則該值被認為是可迭代的(iterable)。 迭代器是一個通過其方法·
next()`返回值的物件。 我們說:它迭代可迭代的項(內容),每次呼叫每次返回一個值。 - Consumption (消費):資料消費者使用迭代器檢索他們正在使用的值。
現在來看看,陣列 arr
可以如何消費?首先通過 鍵為 Symbol.iterator
的方法,建立一個迭代器:
const arr = ['a', 'b', 'c']; const iter = arr[Symbol.iterator](); 複製程式碼
然後通過該迭代器的 next()
方法重複檢索 該陣列中的每個項:
> iter.next() { value: 'a', done: false } > iter.next() { value: 'b', done: false } > iter.next() { value: 'c', done: false } > iter.next() { value: undefined, done: true } 複製程式碼
可以看到, next()
返回的每個項都會被包裝在一個物件中, value
值為原陣列中的項值, done
是否完成了該陣列項序列的檢索。
Iterable 和迭代器 是所謂的迭代協議( ofollow,noindex">介面加上使用它們的規則 )的一部分。該協議的一個關鍵特徵是它是順序的:迭代器每次返回一個值。這意味著,如果可迭代資料結構是非線性的(如樹),迭代將使其線性化。
21.3 可迭代資料來源
我將使用for-of迴圈(參見章節for-of迴圈)迭代各種可迭代資料。
21.3.1 陣列
陣列(和Typed Arrays)可迭代其元素:
for ( const [ 'a' , 'b' ]) { console .log ( x ); } // Output: // 'a' // 'b' 複製程式碼
21.3.2 字串
字串是可迭代的,但它們遍歷Unicode程式碼點,每個程式碼點可能包含一個或兩個JavaScript字元:
for (const x of 'a\uD83D\uDC0A') { console.log(x); } // Output: // 'a' // '\uD83D\uDC0A' (crocodile emoji) 複製程式碼
您剛剛看到原始值也可以迭代。所以不是要求一個是物件,才是可迭代的。 這是因為在訪問迭代器方法(屬性鍵 Symbol.iterator
)之前,所有值都被強制轉換為物件。
21.3.3 Maps
對映 是對其條目的迭代。 每個條目編碼為[key,value]對,具有兩個元素的Array。 這些條目總是以確定的方式迭代,其順序與它們被新增到 這個對映時的順序相同。
const map = new Map().set('a', 1).set('b', 2); for (const pair of map) { console.log(pair); } // Output: // ['a', 1] // ['b', 2] 複製程式碼
請注意,WeakMaps 不可迭代。
21.3.4 Sets
集合是對其元素的迭代(以與它們新增到集合相同的順序迭代)。
const set = new Set().add('a').add('b'); for (const x of set) { console.log(x); } // Output: // 'a' // 'b' // 'b' 複製程式碼
請注意,WeakSets不可迭代。
21.3.5 arguments
儘管特殊變數 arguments
在ECMAScript 6中或多或少已經過時(由於 rest引數),但它是可迭代的:
function printArgs() { for (const x of arguments) { console.log(x); } } printArgs('a', 'b'); // Output: // 'a' // 'b' 複製程式碼
21.3.6 DOM 資料結構
大多數DOM資料結構最終都是可迭代的:
for (const node of document.querySelectorAll('div')) { ··· } 複製程式碼
請注意,實現此功能正在進行中。 但這樣做相對容易,因為符號 Symbol.iterator
不會與現有的屬性鍵衝突。
21.3.7 可變計算資料
並非所有可迭代內容都必須來自資料結構,它也可以即時計算。 例如,所有主要的ES6資料結構(Arrays, Typed Arrays, Maps, Sets)都有三個返回可迭代物件的方法:
entries() keys() values()
讓我們看看它是什麼樣的。 entries()
為您提供了獲取Array元素及其索引的好方法:
const arr = ['a', 'b', 'c']; for (const pair of arr.entries()) { console.log(pair); } // Output: // [0, 'a'] // [1, 'b'] // [2, 'c'] 複製程式碼
21.3.8 普通物件不可迭代
普通物件(由物件字面量建立)不可迭代:
for (const x of {}) { // TypeError console.log(x); } 複製程式碼
預設情況下,為什麼物件不能在屬性上迭代? 推理如下。 您可以在JavaScript中迭代兩個級別:
- 程式級:迭代屬性意味著檢查程式的結構。
- 資料級別:迭代資料結構意味著檢查程式管理的資料。
對屬性進行迭代預設意味著混合這些級別,這將有兩個缺點:
- 您無法迭代資料結構的屬性。
- 迭代物件的屬性後,將該物件轉換為資料結構會破壞您的程式碼。
如果引擎要通過方法 Object.prototype[Symbol.iterator]()
實現迭代,那麼還會有一個警告:通過 Object.create(null)
建立的物件將不可迭代,因為 Object.prototype
不在他們的原型鏈。
重要的是要記住,如果將objects 用作Maps,則迭代物件的屬性大多是有趣的。 但我們只在ES5中這樣做,那時我們沒有更好的選擇。 在ECMAScript 6中,我們有內建的資料結構 Map
。
21.3.8.1 如何迭代屬性
迭代屬性的正確(和安全)方法是通過工具函式。 例如,通過 objectEntries()
, 它的實現將在後面顯示(未來的ECMAScript版本可能內建了類似的東西):
const obj = { first: 'Jane', last: 'Doe' }; for (const [key,value] of objectEntries(obj)) { console.log(`${key}: ${value}`); } // Output: // first: Jane // last: Doe 複製程式碼
21.4 迭代語言結構
以下ES6語言構造使用迭代協議:
- 通過陣列模式進行解構
-
for-of
迴圈 -
Array.from()
- 展開運算子(
...
) - Maps 和Sets的構造器
-
Promise.all()
,Promise.race()
-
yield*
接下來的部分將詳細介紹
21.4.1 通過陣列模式進行解構
通過陣列模式進行解構適用於任何可迭代:
const set = new Set().add('a').add('b').add('c'); const [x,y] = set; // x='a'; y='b' const [first, ...rest] = set; // first='a'; rest=['b','c']; 複製程式碼
21.4.2 for-of迴圈
for-of
是ECMAScript 6中的一個新迴圈。它的基本形式如下所示:
for (const x of iterable) { ··· } 複製程式碼
有關更多資訊,請檢視for-of迴圈。
請注意,iterable 的 可迭代性是必需的,否則 for-of
不能迴圈值。 這意味著必須將非可迭代值轉換為可迭代的值。 例如,通過 Array.from()
。
21.4.3 Array.from()
Array.from()
將可迭代和類似 Array 的值轉換為 Arrays。 它也適用於typed Arrays。
> Array.from(new Map().set(false, 'no').set(true, 'yes')) [[false,'no'], [true,'yes']] > Array.from({ length: 2, 0: 'hello', 1: 'world' }) ['hello', 'world'] 複製程式碼
有關 Array.from()
更多資訊,請參閱有關陣列的章節 。
21.4.4 展開運算子( ... )
spread運算子將iterable的值插入到Array中:
> const arr = ['b', 'c']; > ['a', ...arr, 'd'] ['a', 'b', 'c', 'd'] 複製程式碼
這意味著它為您提供了一種將任何迭代轉換為陣列的簡便方式:
const arr = [... iterable ]; 複製程式碼
展開運算子還將 iterable 轉換為函式,方法或建構函式呼叫的引數:
> Math.max(...[-1, 8, 3]) 8 複製程式碼
21.4.5 Maps 和Sets 建構函式
Map的建構函式將 [key,value] 對上的可迭代變為Map:
> const map = new Map([['uno', 'one'], ['dos', 'two']]); > map.get('uno') 'one' > map.get('dos') 'two' 複製程式碼
Set的建構函式將可迭代的元素轉換為Set:
> const set = new Set(['red', 'green', 'blue']); > set.has('red') true > set.has('yellow') false 複製程式碼
WeakMap 和WeakSet 的建構函式的工作方式類似。此外,Maps 和Sets 本身是可迭代的(WeakMaps 和WeakSets 不是),這意味著您可以使用它們的建構函式來克隆它們。
21.4.6 Promises
Promise.all()
和 Promise.race()
接受 Promises上的迭代:
Promise.all(iterableOverPromises).then(···); Promise.race(iterableOverPromises).then(···); 複製程式碼
21.4.7 yield*
yield*
是僅在生成器內可用的運算子。 它產生迭代物件所迭代的所有項。
function* yieldAllValuesOf(iterable) { yield* iterable; } 複製程式碼
yield*
最重要的用例是遞迴呼叫生成器(生成可迭代的東西)。
21.5 實現迭代
在本節中,我將詳細解釋如何實現iterables。請注意,ES6生成器通常比“手動”更方便去實現。
迭代協議如下所示:

如果物件具有其鍵為 Symbol.iterator
的方法(自己的或繼承的),則該物件變為可迭代 (“實現” Iterable )。 該方法必須返回一個迭代器 ,一個通過其方法 next()
迭代的“內部” 項的物件。
在 TypeScript 表示中,iterables 和迭代器的介面如下所示:
interface Iterable { [Symbol.iterator]() : Iterator; } interface Iterator { next() : IteratorResult; return?(value? : any) : IteratorResult; } interface IteratorResult { value: any; done: boolean; } 複製程式碼
return()
是一個可選的方法,我們將在以後使用。讓我們首先實現一個模擬的迭代,以瞭解迭代的工作原理。
const iterable = { [Symbol.iterator]() { let step = 0 const iterator = { next() { if (step <= 2) { step++ } switch (step) { case 1: return { value: 'hello', done: false } case 2: return { value: 'world', done: false } default: return { value: undefined, done: true } } } } return iterator } } 複製程式碼
讓我們檢查一下, iterable
實際上是可迭代的:
for (const x of iterable) { console.log(x) } // Output: // hello // world 複製程式碼
程式碼執行三個步驟,計數器 step 確保一切都以正確的順序發生。 首先,我們返回值'hello' ,然後返回值'world' ,然後我們指示已經迭代結束。 每個專案都包含在一個具有以下屬性的物件中:
value done
如果為 false
,則可以省略 done
;如果 undefined
,則可以省略 value
。 也就是說, switch
語句可以寫成如下。
switch (step) { case 1: return { value: 'hello' } case 2: return { value: 'world' } default: return { done: true } } 複製程式碼
正如 生成器的章節中所解釋的那樣,在某些情況下,您甚至需要最後一項 done: true
才能獲得 value。 否則, next()
可以更簡單並直接返回專案(不將它們包裝在物件中)。 然後通過特殊值(例如,一個 symbol)指示迭代的結束。
讓我們再看一個可迭代的實現。 函式 iterateOver()
在通過傳遞給它的引數,返回一個 iterable:
function iterateOver(...args) { let index = 0 const iterable = { [Symbol.iterator]() { const iterator = { next() { if (index < args.length) { return { value: args[index++] } } else { return { done: true } } } } return iterator } } return iterable } // Using `iterateOver()`: for (const x of iterateOver('fee', 'fi', 'fo', 'fum')) { console.log(x) } // Output: // fee // fi // fo // fum 複製程式碼
21.5.1 可迭代的迭代器
如果可迭代物件 和迭代器是同一個物件,則可以簡化前一個函式:
function iterateOver(...args) { let index = 0 const iterable = { [Symbol.iterator]() { return this }, next() { if (index < args.length) { return { value: args[index++] } } else { return { done: true } } } } return iterable } 複製程式碼
即使原始的 iterable 和迭代器不是同一個物件,如果迭代器具有以下方法(這也使它成為可迭代的),它偶爾會有用:
[Symbol.iterator]() { return this; } 複製程式碼
所有內建的 ES6 迭代器都遵循這種模式(通過一個通用的原型,參見有關生成器的章節 )。 例如,Arrays 的預設迭代器:
> const arr = []; > const iterator = arr[Symbol.iterator](); > iterator[Symbol.iterator]() === iterator > true 複製程式碼
如果迭代器也是可迭代的,那麼它有什麼用呢? for-of
僅適用於 iterables,不適用於迭代器。 因為 Array 迭代器是可迭代的,所以可以在另一個迴圈中繼續迭代:
const arr = ['a', 'b'] const iterator = arr[Symbol.iterator]() for (const x of iterator) { console.log(x) // a break } // Continue with same iterator: for (const x of iterator) { console.log(x) // b } 複製程式碼
繼續迭代的一個用例是,您可以在通過 for-of
處理實際內容之前刪除初始項(例如標題)。
21.5.2 可選的迭代器方法: return()
和 throw()
兩個迭代器方法是可選的:
- 如果迭代過早結束,則
return()
為迭代器提供清理的機會。 -
throw()
是關於將方法呼叫轉發給通過yield*
迭代的生成器。 有關生成器的章節對此進行了解釋。
21.5.2.1 通過 return()
關閉迭代器
如前所述,可選的迭代器方法 return()
是關於如果迭代器沒有迭代直到結束 而讓迭代器清理的。 它關閉了一個迭代器。 在 for-of 迴圈中,過早(或突然 ,在規範語言中)終止可能由以下原因引起:
- break
- continue (如果您繼續外部迴圈,則
continue
的作用類似於break
) - throw
- return
在每種情況下, for-of
讓迭代器知道迴圈不會完成。 讓我們看一個例子,一個函式 readLinesSync
,它返回一個檔案中的可迭代文字行,並且無論發生什麼都想關閉該檔案:
function readLinesSync(fileName) { const file = ···; return { ··· next() { if (file.isAtEndOfFile()) { file.close(); return { done: true }; } ··· }, return() { file.close(); return { done: true }; }, }; } 複製程式碼
由於 return()
,檔案將在以下迴圈中正確關閉:
// Only print first line for (const line of readLinesSync(fileName)) { console.log(x) break } 複製程式碼
return()
方法必須返回一個物件。這是由於生成器處理 return
語句的方式造成的,有關生成器的章節將對此進行解釋。
以下構造關閉未完全“耗盡”的迭代器:
- for-of
- yield*
- Destructuring
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()
- Promise.all(), Promise.race()
稍後的部分將提供關於關閉迭代器的更多資訊。
21.6 可迭代的更多例子
在本節中,我們將看一些可迭代的例子。 大多數這些迭代更容易通過生成器實現。關於生成器的一章展示瞭如何實現。
21.6.1 返回 iterables 的工具函式
返回可迭代的工具函式和方法與可迭代資料結構一樣重要。 以下是用於迭代物件的自身屬性的工具函式。
function objectEntries(obj) { let index = 0 // In ES6, you can use strings or symbols as property keys, // Reflect.ownKeys() retrieves both const propKeys = Reflect.ownKeys(obj) return { [Symbol.iterator]() { return this }, next() { if (index < propKeys.length) { const key = propKeys[index] index++ return { value: [key, obj[key]] } } else { return { done: true } } } } } const obj = { first: 'Jane', last: 'Doe' } for (const [key, value] of objectEntries(obj)) { console.log(`${key}: ${value}`) } // Output: // first: Jane // last: Doe 複製程式碼
另一種選擇是使用迭代器而不是索引來遍歷具有屬性鍵的陣列:
function objectEntries(obj) { let iter = Reflect.ownKeys(obj)[Symbol.iterator]() return { [Symbol.iterator]() { return this }, next() { let { done, value: key } = iter.next() if (done) { return { done: true } } return { value: [key, obj[key]] } } } } 複製程式碼
21.6.2 迭代的組合器
組合器 是組合現有迭代(iterables)來建立新迭代的函式。
21.6.2.1 take(n, iterable)
讓我們從組合函式 take(n, iterable)
,它返回可迭代的前 n
項的 iterable
。
function take(n, iterable) { const iter = iterable[Symbol.iterator]() return { [Symbol.iterator]() { return this }, next() { if (n > 0) { n-- return iter.next() } else { return { done: true } } } } } const arr = ['a', 'b', 'c', 'd'] for (const x of take(2, arr)) { console.log(x) } // Output: // a // b 複製程式碼
這個版本的 take()
不會關閉迭代器 iter
。在我解釋了關閉迭代器的實際含義之後,稍後將展示如何做到這一點。
21.6.2.2 zip(...iterables)
zip
將 n
個可迭代項轉換為 n
元組(編碼為長度為 n
的陣列)的可迭代項。。
function zip(...iterables) { const iterators = iterables.map(i => i[Symbol.iterator]()) let done = false return { [Symbol.iterator]() { return this }, next() { if (!done) { const items = iterators.map(i => i.next()) done = items.some(item => item.done) if (!done) { return { value: items.map(i => i.value) } } // Done for the first time: close all iterators for (const iterator of iterators) { if (typeof iterator.return === 'function') { iterator.return() } } } // We are done return { done: true } } } } 複製程式碼
如您所見,最短的 iterable
決定了結果的長度:
const zipped = zip(['a', 'b', 'c'], ['d', 'e', 'f', 'g']) for (const x of zipped) { console.log(x) } // Output: // ['a', 'd'] // ['b', 'e'] // ['c', 'f'] 複製程式碼
21.6.3 無限可迭代
有些迭代可能永遠不會 done 。
function naturalNumbers() { let n = 0 return { [Symbol.iterator]() { return this }, next() { return { value: n++ } } } } 複製程式碼
對於無限迭代,您一定不要去遍歷它的所有項。例如,通過從 for-of
迴圈中斷開
for (const x of naturalNumbers()) { if (x > 2) break //這裡進行中斷 console.log(x) } 複製程式碼
或者只訪問無限可迭代的開頭:
const [a, b, c] = naturalNumbers() // a=0; b=1; c=2; 複製程式碼
或者使用組合器。 take()
是一種可能性:
for (const x of take(3, naturalNumbers())) { console.log(x) } // Output: // 0 // 1 // 2 複製程式碼
zip()
返回的 iterable 的“長度”由其最短的輸入可迭代決定。 這意味著 zip()
和 naturalNumbers()
為您提供了對任意(有限)長度的迭代器進行編號的方法:
const zipped = zip(['a', 'b', 'c'], naturalNumbers()) for (const x of zipped) { console.log(x) } // Output: // ['a', 0] // ['b', 1] // ['c', 2] 複製程式碼
21.7 FAQ:iterables 和 iterators
21.7.1 迭代協議不是很慢嗎?
您可能會擔心迭代協議很慢,因為每次呼叫 next()都會建立一個新物件。然而,對於小物件的記憶體管理在現代引擎中是快速的,從長遠來看,引擎可以優化迭代,這樣就不需要分配中間物件。關於 es-discuss 上的一個帖子有更多的資訊。
21.7.2 我可以多次重複使用同一個物件嗎?
原則上,沒有什麼能阻止迭代器多次重複使用相同的迭代結果物件 - 我希望大多數事情都能正常工作。 但是,如果客戶端快取迭代結果,則會出現問題:
const iterationResults = [] const iterator = iterable[Symbol.iterator]() let iterationResult while (!(iterationResult = iterator.next()).done) { iterationResults.push(iterationResult) } 複製程式碼
如果迭代器重用其迭代結果物件,則 iterationResults
通常會多次包含同一個物件。
21.7.3 為什麼 ECMAScript 6 沒有可迭代的組合器?
您可能想知道為什麼 ECMAScript 6 沒有可迭代的組合器 ,用於處理迭代的工具或用於建立迭代的工具。 那是因為計劃分兩步進行:
- 第 1 步:標準化迭代協議。
- 第 2 步:根據該協議等待庫。
最終,一個這樣的庫或來自幾個庫的片段將被新增到 JavaScript 標準庫中。
如果您想了解這樣的庫可能是什麼樣子,請檢視標準 Python 模組 itertools
。
21.7.4 難道迭代(iterables)很難實現嗎?
是的,迭代很難實現 - 如果你手動去實現它們。 下一章將介紹有助於完成此任務的生成器 (以及其他內容)。
##21.8 深入的 ECMAScript 6 迭代協議 迭代協議包含以下介面(我省略了 Iterator 中的 throw()
,它只受 yield*
支援,並且是可選的):
interface Iterable { [Symbol.iterator]() : Iterator; } interface Iterator { next() : IteratorResult; return?(value? : any) : IteratorResult; } interface IteratorResult { value : any; done : boolean; } 複製程式碼
規範有一個關於迭代協議的部分 。
21.8.1 迭代
next()
規則:
- 只要迭代器仍然具有要生成的值
x
,next()
返回物件{ value: x, done: false }
。 - 迭代完最後一個值之後,
next()
應該總是返回一個屬性為 true 的物件。
21.8.1.1 IteratorResult
迭代器結果的屬性不必是 true
或 false
,能夠代表真假進行判斷就是足夠的。所有內建語言機制都允許您省略 done: false 。
21.8.1.2 返回新迭代器的 Iterables 與總是返回相同迭代器的 Iterables
一些 iterables 每次被要求生成一個新的迭代器。 例如,陣列:
function getIterator(iterable) { return iterable[Symbol.iterator]() } const iterable = ['a', 'b'] console.log(getIterator(iterable) === getIterator(iterable)) // false 複製程式碼
其他迭代每次都返回相同的迭代器。 例如,生成器物件:
function* elements() { yield 'a' yield 'b' } const iterable = elements() console.log(getIterator(iterable) === getIterator(iterable)) // true 複製程式碼
當您多次迭代同一迭代器時,可迭代(iterable)是否產生新的迭代器並不重要。例如,通過以下函式:
function iterateTwice(iterable) { for (const x of iterable) { console.log(x) } for (const x of iterable) { console.log(x) } } 複製程式碼
使用新的迭代器,您可以多次迭代相同的可迭代:
iterateTwice(['a', 'b']) // Output: // a // b // a // b 複製程式碼
如果每次都返回相同的迭代器,則不能:
iterateTwice(elements()) // Output: // a // b 複製程式碼
請注意,標準庫中的每個迭代器也是可迭代的。 它的方法 [Symbol.iterator]()
返回 this
,這意味著它總是返回相同的迭代器(本身)。
21.8.2 關閉迭代器
迭代協議區分了兩種完成迭代器的方法:
- 耗盡(Exhaustion):完成迭代器的常規方法是檢索其所有值。也就是說,一直呼叫
next()
直到它返回一個屬性done
為true
的物件。 - 關閉(Closing):通過呼叫
return()
,告訴迭代器你不打算再呼叫 next() 。
呼叫 return()
規則:
-
return()
是一個可選方法,並非所有迭代器都有它。 具有它的迭代器被稱為可關閉的 。 - 只有在迭代器沒有用盡時才應該呼叫
return()
。 例如,只要“突然”(在它完成之前return()
,for-of
呼叫return()
)。 以下操作會導致突然退出:break
,continue
(帶有外部塊的標籤),return
,throw
。
實現 return()
規則:
- 方法呼叫
return(x)
通常應該生成物件{ done: true, value: x }
,但是如果結果不是物件,語言機制只會丟擲錯誤(source in spec)。 - 呼叫
return()
後,next()
返回的物件也應該done
。
下面的程式碼說明了 for-of
迴圈 如果在收到 一個 done
迭代器結果 之前中止它,則它呼叫 return()
。 也就是說,如果在收到最後一個值後中止,則甚至會呼叫 return() 。 這是微妙的,當您手動迭代或實現迭代器時,您必須小心謹慎。
function createIterable() { let done = false const iterable = { [Symbol.iterator]() { return this }, next() { if (!done) { done = true return { done: false, value: 'a' } } else { return { done: true, value: undefined } } }, return() { console.log('return() was called!') } } return iterable } for (const x of createIterable()) { console.log(x) // There is only one value in the iterable and // we abort the loop after receiving it break } // Output: // a // return() was called! 複製程式碼
21.8.2.1 可關閉的迭代器
如果迭代器具有方法 return()
則它是可關閉的。並非所有迭代器都可以關閉。例如,Array 迭代器不是:
> let iterable = ['a', 'b', 'c']; > const iterator = iterable[Symbol.iterator](); > 'return' in iterator false 複製程式碼
預設情況下,Generator 物件是可關閉的。 例如,由以下生成器函式返回的:
function* elements() { yield 'a' yield 'b' yield 'c' } 複製程式碼
如果在 elements()
的結果上呼叫 return()
,則迭代完成:
> const iterator = elements(); > iterator.next() { value: 'a', done: false } > iterator.return() { value: undefined, done: true } > iterator.next() { value: undefined, done: true } 複製程式碼
如果迭代器不可關閉,則可以在 for-of
迴圈中突然退出(例如 A 行中的那個)之後繼續迭代它:
function twoLoops(iterator) { for (const x of iterator) { console.log(x) break // (A) } for (const x of iterator) { console.log(x) } } function getIterator(iterable) { return iterable[Symbol.iterator]() } twoLoops(getIterator(['a', 'b', 'c'])) // Output: // a // b // c 複製程式碼
相反, elements()
返回一個可關閉的迭代器,而 twoLoops()
內的第二個迴圈沒有任何可迭代的東西:
function* elements() { yield 'a' yield 'b' yield 'c' } function twoLoops(iterator) { for (const x of iterator) { console.log(x) break // (A) } for (const x of iterator) { console.log(x) } } twoLoops(elements()) // Output: // a 複製程式碼
21.8.2.2 防止迭代器被關閉
以下類是防止迭代器被關閉的通用解決方案。它通過包裝迭代器和轉發除 return()
之外的所有方法呼叫來實現這一點。
class PreventReturn { constructor(iterator) { this.iterator = iterator } /** Must also be iterable, so that for-of works */ [Symbol.iterator]() { return this } next() { return this.iterator.next() } return(value = undefined) { return { done: false, value } } // Not relevant for iterators: `throw()` } 複製程式碼
如果我們使用 PreventReturn
,那麼在 twoLoops()
的第一個迴圈中突然退出後,生成器 elements()
的結果將不會被關閉。
function* elements() { yield 'a' yield 'b' yield 'c' } function twoLoops(iterator) { for (const x of iterator) { console.log(x) break // abrupt exit } for (const x of iterator) { console.log(x) } } twoLoops(elements()) // Output: // a twoLoops(new PreventReturn(elements())) // Output: // a // b // c 複製程式碼
還有另一種使生成器不可關閉的方法:生成器函式 elements()
生成的所有生成器物件都具有原型物件 elements.prototype
。 通過 elements.prototype
,您可以隱藏 return()
的預設實現(它駐留在 elements.prototype
的原型中),如下所示:
// Make generator object unclosable // Warning: may not work in transpilers elements.prototype.return = undefined twoLoops(elements()) // Output: // a // b // c 複製程式碼
21.8.2.3 通過 try-finally
對生成器進行清理
一些生成器需要在迭代完成後清理(釋放已分配的資源,關閉開啟的檔案等)。這就是我們實現它的方式:
function* genFunc() { yield 'a' yield 'b' console.log('Performing cleanup') } 複製程式碼
在正常的 for-of 迴圈中,一切都很好:
for (const x of genFunc()) { console.log(x) } // Output: // a // b // Performing cleanup 複製程式碼
但是,如果在第一次 yield 後退出迴圈,則執行似乎永遠停留在那裡並且永遠不會到達清理步驟:
for (const x of genFunc()) { console.log(x) break } // Output: // a 複製程式碼
實際發生的情況是,每當提前離開 for-of
迴圈時, for-of
都會向當前迭代器傳送 return()
。這意味著沒有完成清理步驟,因為生成器函式就提前返回。
值得慶幸的是,通過在 finally
子句中執行清理可以很容易地解決這個問題:
function* genFunc() { try { yield 'a' yield 'b' } finally { console.log('Performing cleanup') } } 複製程式碼
現在一切都按預期工作:
for (const x of genFunc()) { console.log(x) break } // Output: // a // Performing cleanup 複製程式碼
因此,使用需要以某種方式關閉或清理的資源的一般模式是:
function* funcThatUsesResource() { const resource = allocateResource(); try { ··· } finally { resource.deallocate(); } } 複製程式碼
21.8.2.4 在手動實現的迭代器中處理清理
const iterable = { [Symbol.iterator]() { function hasNextValue() { ··· } function getNextValue() { ··· } function cleanUp() { ··· } let returnedDoneResult = false; return { next() { if (hasNextValue()) { const value = getNextValue(); return { done: false, value: value }; } else { if (!returnedDoneResult) { // Client receives first `done` iterator result // => won’t call `return()` cleanUp(); returnedDoneResult = true; } return { done: true, value: undefined }; } }, return() { cleanUp(); } }; } } 複製程式碼
注意,當您第一次返回一個 done
迭代器結果時,必須呼叫 cleanUp()
。您不能提前地執行,因為 return()
可能仍然會被呼叫。為了做到這一點,可能很棘手。
21.8.2.5 關閉你使用的迭代器
如果使用迭代器,則應正確關閉它們。在生成器中,您可以讓 for-of 所有工作為您完成:
/** * Converts a (potentially infinite) sequence of * iterated values into a sequence of length `n` */ function* take(n, iterable) { for (const x of iterable) { if (n <= 0) { break // closes iterable } n-- yield x } } 複製程式碼
如果您手動去管理,需要做一些工作:
function* take(n, iterable) { const iterator = iterable[Symbol.iterator]() while (true) { const { value, done } = iterator.next() if (done) break // exhausted if (n <= 0) { // Abrupt exit maybeCloseIterator(iterator) break } yield value n-- } } function maybeCloseIterator(iterator) { if (typeof iterator.return === 'function') { iterator.return() } } 複製程式碼
如果不使用生成器,則需要做更多工作:
function take(n, iterable) { const iter = iterable[Symbol.iterator]() return { [Symbol.iterator]() { return this }, next() { if (n > 0) { n-- return iter.next() } else { maybeCloseIterator(iter) return { done: true } } }, return() { n = 0 maybeCloseIterator(iter) } } } 複製程式碼