21. 可迭代对象和迭代器
目录
请支持本书:购买 (PDF, EPUB, MOBI)捐赠
(广告,请不要屏蔽。)

21. 可迭代对象和迭代器



21.1 概述

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

用 TypeScript 表示法表示为接口,这些角色如下所示

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
}
interface IteratorResult {
    value: any;
    done: boolean;
}

21.1.1 可迭代值

以下值是可迭代的

普通对象不可迭代(原因在专门章节中解释)。

21.1.2 支持迭代的构造函数

通过迭代访问数据的语言结构

21.2 可迭代性

可迭代性的概念如下。

每个消费者都支持所有来源是不切实际的,特别是因为应该可以创建新的来源(例如,通过库)。因此,ES6 引入了接口 Iterable。数据消费者使用它,数据源实现它

鉴于 JavaScript 没有接口,Iterable 更像是一种约定

让我们看看数组 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 数组

数组(和类型化数组)在其元素上是可迭代的

for (const x of ['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)

21.3.3 Map

Map 在其条目上是可迭代的。每个条目都被编码为一个 [key, value] 对,一个包含两个元素的数组。条目总是以确定性的方式进行迭代,顺序与它们添加到 Map 中的顺序相同。

const map = new Map().set('a', 1).set('b', 2);
for (const pair of map) {
    console.log(pair);
}
// Output:
// ['a', 1]
// ['b', 2]

请注意,WeakMap 不可迭代。

21.3.4 Set

Set 在其元素上是可迭代的(迭代顺序与它们添加到 Set 中的顺序相同)。

const set = new Set().add('a').add('b');
for (const x of set) {
    console.log(x);
}
// Output:
// 'a'
// 'b'

请注意,WeakSet 不可迭代。

21.3.5 arguments

尽管特殊变量 arguments 在 ECMAScript 6 中或多或少已经过时(由于剩余参数),但它是可迭代的

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 数据结构(数组、类型化数组、Map、Set)都有三个返回可迭代对象的方法

让我们看看它是什么样子的。entries() 为您提供了一种获取数组元素及其索引的好方法

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 中进行迭代的级别有两个

  1. 程序级别:迭代属性意味着检查程序的结构。
  2. 数据级别:迭代数据结构意味着检查程序管理的数据。

将属性迭代设为默认值意味着混合这些级别,这有两个缺点

如果引擎要通过方法 Object.prototype[Symbol.iterator]() 实现可迭代性,那么还有一个额外的注意事项:通过 Object.create(null) 创建的对象将不可迭代,因为 Object.prototype 不在它们的原型链中。

重要的是要记住,如果您将对象用作 Map1,则迭代对象的属性主要是有趣的。但我们只在 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 语言结构利用了迭代协议

以下部分详细描述了它们中的每一个。

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.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 展开运算符 (...)

展开运算符将可迭代对象的值插入数组中

> const arr = ['b', 'c'];
> ['a', ...arr, 'd']
['a', 'b', 'c', 'd']

这意味着它为您提供了一种将任何可迭代对象转换为数组的紧凑方法

const arr = [...iterable];

展开运算符还将可迭代对象转换为函数、方法或构造函数调用的参数

> Math.max(...[-1, 8, 3])
8

21.4.5 Map 和 Set

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

WeakMapWeakSet 的构造函数的工作方式类似。此外,Map 和 Set 本身是可迭代的(WeakMap 和 WeakSet 不是),这意味着您可以使用它们的构造函数来克隆它们。

21.4.6 Promise

Promise.all()Promise.race() 接受 Promise 的可迭代对象

Promise.all(iterableOverPromises).then(···);
Promise.race(iterableOverPromises).then(···);

21.4.7 yield*

yield* 是一个只能在生成器内部使用的运算符。它会 yield 由可迭代对象迭代的所有项目。

function* yieldAllValuesOf(iterable) {
    yield* iterable;
}

yield* 最重要的用例是递归调用生成器(生成可迭代对象)。

21.5 实现可迭代对象

在本节中,我将详细解释如何实现可迭代对象。请注意,ES6 生成器 通常比“手动”执行此任务方便得多。

迭代协议如下所示。

如果一个对象有一个键为 Symbol.iterator 的方法(自己的或继承的),则该对象变为_可迭代的_(“实现”接口 Iterable)。该方法必须返回一个_迭代器_,该迭代器是一个通过其方法 next()_迭代_可迭代对象“内部”_项目_的对象。

在 TypeScript 表示法中,可迭代对象和迭代器的接口如下所示2

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
    return?(value? : any) : IteratorResult;
}
interface IteratorResult {
    value: any;
    done: boolean;
}

return() 是一个可选方法,我们稍后会介绍3。让我们首先实现一个虚拟可迭代对象,以了解迭代的工作原理。

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' 值,最后我们指示迭代已到达末尾。每个项目都包装在一个具有以下属性的对象中

如果 donefalse,则可以省略它;如果 valueundefined,则可以省略它。也就是说,switch 语句可以写成如下形式。

switch (step) {
    case 1:
        return { value: 'hello' };
    case 2:
        return { value: 'world' };
    default:
        return { done: true };
}

正如关于生成器的章节中所解释的,在某些情况下,您甚至希望最后一个带有 done: true 的项目也具有 value。否则,next() 可以更简单,直接返回项目(不将它们包装在对象中)。然后,迭代的结束将通过特殊值(例如,符号)来指示。

让我们再来看一个可迭代对象的实现。函数 iterateOver() 返回传递给它的参数的可迭代对象

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;
}

即使原始可迭代对象和迭代器不是同一个对象,如果迭代器具有以下方法(这也使其成为可迭代的),则它仍然偶尔有用

[Symbol.iterator]() {
    return this;
}

所有内置 ES6 迭代器都遵循此模式(通过公共原型,请参阅关于生成器的章节)。例如,数组的默认迭代器

> const arr = [];
> const iterator = arr[Symbol.iterator]();
> iterator[Symbol.iterator]() === iterator
true

为什么如果迭代器也是可迭代的,这会很有用?for-of 仅适用于可迭代对象,而不适用于迭代器。因为数组迭代器是可迭代的,所以您可以在另一个循环中继续迭代

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

两个迭代器方法是可选的

21.5.2.1 通过 return() 关闭迭代器

如前所述,可选的迭代器方法 return() 是关于让迭代器在没有迭代到末尾时进行清理。它_关闭_迭代器。在 for-of 循环中,过早(或规范语言中的_突然_)终止可能是由以下原因造成的

在每种情况下,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 语句的方式,将在关于生成器的章节中进行解释。

以下构造会关闭未完全“耗尽”的迭代器

后面的章节提供了有关关闭迭代器的更多信息。

21.6 更多可迭代对象的例子

在本节中,我们将查看更多可迭代对象的例子。大多数这些可迭代对象更容易通过生成器实现。关于生成器的章节展示了如何实现。

21.6.1 返回可迭代对象的工具函数

返回可迭代对象的工具函数和方法与可迭代数据结构一样重要。以下是用于迭代对象自身属性的工具函数。

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 可迭代对象的组合器

_组合器_4 是将现有可迭代对象组合起来创建新的可迭代对象的函数。

21.6.2.1 take(n, iterable)

让我们从组合器函数 take(n, iterable) 开始,它返回 iterable 的前 n 个项目的可迭代对象。

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
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 };
        }
    }
}

如您所见,最短的可迭代对象决定了结果的长度

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 无限可迭代对象

有些可迭代对象可能永远不会_完成_。

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() 返回的可迭代对象的“长度”由其最短的输入可迭代对象决定。这意味着 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 常见问题解答:可迭代对象和迭代器

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 没有_可迭代对象组合器_,即用于处理可迭代对象或创建可迭代对象的工具。这是因为计划分两步进行

最终,一个这样的库或来自多个库的部分将被添加到 JavaScript 标准库中。

如果您想了解此类库的外观,请查看标准 Python 模块 itertools

21.7.4 实现可迭代对象不难吗?

是的,可迭代对象很难实现——如果您手动实现它们的话。下一章将介绍_生成器_,它们可以帮助完成这项任务(以及其他任务)。

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() 的规则

21.8.1.1 IteratorResult

迭代器结果的属性 done 不必是 truefalse,真值或假值就足够了。所有内置语言机制都允许您省略 done: false

21.8.1.2 返回新迭代器的可迭代对象与始终返回相同迭代器的可迭代对象

某些可迭代对象每次被请求时都会生成一个新的迭代器。例如,数组

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

可迭代对象是生成新的迭代器还是不生成新的迭代器,这取决于您是否多次迭代同一个可迭代对象。例如,通过以下函数

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 关闭迭代器

迭代协议区分两种完成迭代器的方式

调用 return() 的规则

实现 return() 的规则

以下代码说明了如果在收到 done 迭代器结果之前中止 for-of 循环,则 for-of 循环会调用 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() 方法,则它是*可关闭的*。并非所有迭代器都是可关闭的。例如,数组迭代器不是

> let iterable = ['a', 'b', 'c'];
> const iterator = iterable[Symbol.iterator]();
> 'return' in iterator
false

生成器对象默认是可关闭的。例如,以下生成器函数返回的对象

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() 内部的第二个循环没有任何可迭代的内容

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 检查表

下一篇:22. 生成器