博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Iterables和迭代器
阅读量:6592 次
发布时间:2019-06-24

本文共 23741 字,大约阅读时间需要 79 分钟。

翻译来源:

21 Iterables和迭代器

21.1 概述

ES6引入了一种遍历数据的新机制:迭代。两个概念是迭代的核心:

  1. 可迭代是一种数据结构,让我们可以方便的访问其元素。它通过实现一个键为 Symbol.iterator 的方法来实现。这是迭代器的工厂方法。
  2. 迭代器是用于遍历数据结构元素的指针(想想数据库中的游标 (cursors ))。

如下是在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 和迭代器 是所谓的迭代协议()的一部分。该协议的一个关键特征是它是顺序的:迭代器每次返回一个值。这意味着,如果可迭代数据结构是非线性的(如树),迭代将使其线性化。

21.3 可迭代数据源

我将使用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不在他们的原型链。

重要的是要记住,如果将,则迭代对象的属性大多是有趣的。 但我们只在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) {        ···    }复制代码

有关更多信息,请查看。

请注意,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。请注意,通常比“手动”更方便去实现。

迭代协议如下所示:

如果对象具有其键为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 linefor (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 iterationResultwhile (!(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 },但是如果结果不是对象,语言机制只会抛出错误()。
  • 调用 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 iteratorfalse复制代码

默认情况下,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:// atwoLoops(new PreventReturn(elements()))// Output:// a// b// c复制代码

还有另一种使生成器不可关闭的方法:生成器函数 elements()生成的所有生成器对象都具有原型对象 elements.prototype 。 通过 elements.prototype,您可以隐藏 return()的默认实现(它驻留在 elements.prototype 的原型中),如下所示:

// Make generator object unclosable// Warning: may not work in transpilerselements.prototype.return = undefinedtwoLoops(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 关闭迭代器,当且仅当您没有耗尽它时。要做到这一点可能很棘手。
  • 在”突然“退出后继续在迭代器上迭代:迭代器必须是不可关闭的或不可关闭的(例如通过工具类)。

原文地址:

转载地址:http://seuio.baihongyu.com/

你可能感兴趣的文章
JavaScript作用域
查看>>
【295天】跃迁之路——程序员高效学习方法论探索系列(实验阶段53-2017.11.27)...
查看>>
Spring之面向切面
查看>>
Cloud + TiDB 技术解读
查看>>
Mysql迁移新环境索引损坏
查看>>
物联网协议之CoAP协议开发学习笔记之常用开源代码实现
查看>>
一些Mac的使用技巧
查看>>
spring event发布及监听实例
查看>>
JavaScript 之银弹の技法
查看>>
html+css+js开发文本编辑器,有各种排版功能!
查看>>
jQTips · 动态添加元素的清爽写法
查看>>
基于Thinkphp5+phpQuery 网络爬虫抓取数据接口,统一输出接口数据api
查看>>
webApp实战开发,仿网易新闻webApp
查看>>
利用css3修改input[type=radio]样式
查看>>
简单的文件缓存函数
查看>>
原生Js判断元素是否隐藏
查看>>
nodejs log4js配置使用
查看>>
Swift 代码小抄
查看>>
git小技巧--如何从其他分支merge个别文件或文件夹
查看>>
微信小程序——gulp处理文件
查看>>