Iterables和迭代器

翻譯來源:exploringjs.com/es6/ch_iter…html

21 Iterables和迭代器

21.1 概述

ES6引入了一種遍歷數據的新機制:迭代。兩個概念是迭代的核心:node

  1. 可迭代是一種數據結構,讓咱們能夠方便的訪問其元素。它經過實現一個鍵爲 Symbol.iterator 的方法來實現。這是迭代器的工廠方法。
  2. 迭代器是用於遍歷數據結構元素的指針(想一想數據庫中的遊標 (cursors ))。

以下是在TypeScript 中用接口來表示的方式:es6

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()bash

    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 和迭代器 是所謂的迭代協議(接口加上使用它們的規則)的一部分。該協議的一個關鍵特徵是它是順序的:迭代器每次返回一個值。這意味着,若是可迭代數據結構是非線性的(如樹),迭代將使其線性化。

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() 返回一個可迭代的條目,編碼爲[key,value] 的Array。 對於Arrays,值是Array元素,鍵是它們的索引。 對於集合,每一個鍵和值都相同 - Set元素。
  • 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)

zipn 個可迭代項轉換爲 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() 規則:

  • 只要迭代器仍然具備要生成的值 xnext()返回對象{ value: x, done: false }
  • 迭代完最後一個值以後, next() 應該老是返回一個屬性爲 true 的對象。

21.8.1.1 IteratorResult

迭代器結果的屬性沒必要是 truefalse,可以表明真假進行判斷就是足夠的。全部內置語言機制都容許您省略 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() 直到它返回一個屬性donetrue 的對象。
  • 關閉(Closing):經過調用 return(),告訴迭代器你不打算再調用 next() 。

調用 return()規則:

  • return() 是一個可選方法,並不是全部迭代器都有它。 具備它的迭代器被稱爲可關閉的 。
  • 只有在迭代器沒有用盡時才應該調用 return() 。 例如,只要「忽然」(在它完成以前 return()for-of 調用 return())。 如下操做會致使忽然退出:breakcontinue(帶有外部塊的標籤), returnthrow

實現 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)
        }
    }
}
複製代碼

21.8.3 清單

  • 記錄可迭代的:提供如下信息。

    • 它是每次返回新的迭代器仍是相同的迭代器?
    • 它的迭代器是可關閉的嗎?
  • 實現迭代器:

    • 若是迭代器耗盡或被 return()調用,則必須進行清理活動。
      • 在生成器中,try-finally 您能夠在一個位置處理這兩種狀況。
    • 經過return() 關閉迭代器後,它不該該經過 next() 生成任何迭代器結果。
  • 手動使用迭代器(經過 for-of 等):

    • 不要忘記經過 return 關閉迭代器,當且僅當您沒有耗盡它時。要作到這一點可能很棘手。
  • 在」忽然「退出後繼續在迭代器上迭代:迭代器必須是不可關閉的或不可關閉的(例如經過工具類)。

原文地址:www.kancloud.cn/chandler/ex…

相關文章
相關標籤/搜索