argumentsfor-of 循环Array.from()...)yield*return() 和 throw()ES6 引入了一种遍历数据的新机制:迭代。迭代的中心是两个概念
Symbol.iterator 的方法来做到这一点。该方法是迭代器的工厂。用 TypeScript 表示法表示为接口,这些角色如下所示
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
}
interface IteratorResult {
value: any;
done: boolean;
}
以下值是可迭代的
普通对象不可迭代(原因在专门章节中解释)。
通过迭代访问数据的语言结构
const [a,b] = new Set(['a', 'b', 'c']);
for-of 循环 for (const x of ['a', 'b', 'c']) {
console.log(x);
}
Array.from():
const arr = Array.from(new Set(['a', 'b', 'c']));
...) const arr = [...new Set(['a', 'b', 'c'])];
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;
可迭代性的概念如下。
for-of 循环遍历值,展开运算符 (...) 将值插入数组或函数调用中。每个消费者都支持所有来源是不切实际的,特别是因为应该可以创建新的来源(例如,通过库)。因此,ES6 引入了接口 Iterable。数据消费者使用它,数据源实现它
鉴于 JavaScript 没有接口,Iterable 更像是一种约定
Symbol.iterator,并且该方法返回一个所谓的迭代器,则该值被认为是可迭代的。迭代器是一个对象,它通过其方法 next() 返回值。我们说:它迭代可迭代对象的项(内容),每次方法调用一个。让我们看看数组 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 和迭代器是所谓的协议(接口加上使用它们的规则)的一部分,用于迭代。此协议的一个关键特征是它是顺序的:迭代器一次返回一个值。这意味着如果可迭代数据结构是非线性的(例如树),则迭代将使其线性化。
我将使用 for-of 循环(参见“for-of 循环”一章)来迭代各种可迭代数据。
数组(和类型化数组)在其元素上是可迭代的
for (const x of ['a', 'b']) {
console.log(x);
}
// Output:
// 'a'
// 'b'
字符串是可迭代的,但它们迭代 Unicode 代码点,每个代码点可能包含一个或两个 JavaScript 字符
for (const x of 'a\uD83D\uDC0A') {
console.log(x);
}
// Output:
// 'a'
// '\uD83D\uDC0A' (crocodile emoji)
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 不可迭代。
Set 在其元素上是可迭代的(迭代顺序与它们添加到 Set 中的顺序相同)。
const set = new Set().add('a').add('b');
for (const x of set) {
console.log(x);
}
// Output:
// 'a'
// 'b'
请注意,WeakSet 不可迭代。
arguments 尽管特殊变量 arguments 在 ECMAScript 6 中或多或少已经过时(由于剩余参数),但它是可迭代的
function printArgs() {
for (const x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// Output:
// 'a'
// 'b'
大多数 DOM 数据结构最终都将是可迭代的
for (const node of document.querySelectorAll('div')) {
···
}
请注意,实现此功能的工作正在进行中。但这相对容易做到,因为符号 Symbol.iterator 不会与现有的属性键冲突。
并非所有可迭代内容都必须来自数据结构,它也可以动态计算。例如,所有主要的 ES6 数据结构(数组、类型化数组、Map、Set)都有三个返回可迭代对象的方法
entries() 返回一个对编码为 [key, value] 数组的条目的可迭代对象。对于数组,值是数组元素,键是它们的索引。对于 Set,每个键和值都是相同的——Set 元素。keys() 返回一个对条目键的可迭代对象。values() 返回一个对条目值的可迭代对象。让我们看看它是什么样子的。entries() 为您提供了一种获取数组元素及其索引的好方法
const arr = ['a', 'b', 'c'];
for (const pair of arr.entries()) {
console.log(pair);
}
// Output:
// [0, 'a']
// [1, 'b']
// [2, 'c']
普通对象(由对象字面量创建)不可迭代
for (const x of {}) { // TypeError
console.log(x);
}
为什么默认情况下对象不能在其属性上进行迭代?原因如下。您可以在 JavaScript 中进行迭代的级别有两个
将属性迭代设为默认值意味着混合这些级别,这有两个缺点
如果引擎要通过方法 Object.prototype[Symbol.iterator]() 实现可迭代性,那么还有一个额外的注意事项:通过 Object.create(null) 创建的对象将不可迭代,因为 Object.prototype 不在它们的原型链中。
重要的是要记住,如果您将对象用作 Map1,则迭代对象的属性主要是有趣的。但我们只在 ES5 中这样做,因为我们没有更好的选择。在 ECMAScript 6 中,我们有内置数据结构 Map。
迭代属性的正确(且安全)方法是通过工具函数。例如,通过 objectEntries(),其实现如下所示(未来的 ECMAScript 版本可能内置了类似的功能)
const obj = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(obj)) {
console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe
以下 ES6 语言结构利用了迭代协议
for-of 循环Array.from()...)Promise.all(), Promise.race()yield*以下部分详细描述了它们中的每一个。
通过数组模式解构适用于任何可迭代对象
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'];
for-of 循环 for-of 是 ECMAScript 6 中的一个新循环。它的基本形式如下所示
for (const x of iterable) {
···
}
有关更多信息,请参阅“for-of 循环”一章。
请注意,需要 iterable 的可迭代性,否则 for-of 无法循环遍历值。这意味着不可迭代的值必须转换为可迭代的值。例如,通过 Array.from()。
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() 的更多信息,请参阅关于数组的章节。
...) 展开运算符将可迭代对象的值插入数组中
> const arr = ['b', 'c'];
> ['a', ...arr, 'd']
['a', 'b', 'c', 'd']
这意味着它为您提供了一种将任何可迭代对象转换为数组的紧凑方法
const arr = [...iterable];
展开运算符还将可迭代对象转换为函数、方法或构造函数调用的参数
> Math.max(...[-1, 8, 3])
8
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 的构造函数的工作方式类似。此外,Map 和 Set 本身是可迭代的(WeakMap 和 WeakSet 不是),这意味着您可以使用它们的构造函数来克隆它们。
Promise.all() 和 Promise.race() 接受 Promise 的可迭代对象
Promise.all(iterableOverPromises).then(···);
Promise.race(iterableOverPromises).then(···);
yield* yield* 是一个只能在生成器内部使用的运算符。它会 yield 由可迭代对象迭代的所有项目。
function* yieldAllValuesOf(iterable) {
yield* iterable;
}
yield* 最重要的用例是递归调用生成器(生成可迭代对象)。
在本节中,我将详细解释如何实现可迭代对象。请注意,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' 值,最后我们指示迭代已到达末尾。每个项目都包装在一个具有以下属性的对象中
value 保存实际项目,done 是一个布尔标志,指示是否已到达末尾。如果 done 为 false,则可以省略它;如果 value 为 undefined,则可以省略它。也就是说,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
如果可迭代对象和迭代器是同一个对象,则可以简化前面的函数
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 处理实际内容之前删除初始项目(例如标题)。
return() 和 throw() 两个迭代器方法是可选的
return() 会让迭代器有机会进行清理。throw() 是关于将方法调用转发给通过 yield* 迭代的生成器。这在关于生成器的章节中进行了解释。return() 关闭迭代器 如前所述,可选的迭代器方法 return() 是关于让迭代器在没有迭代到末尾时进行清理。它_关闭_迭代器。在 for-of 循环中,过早(或规范语言中的_突然_)终止可能是由以下原因造成的
breakcontinue(如果您继续外部循环,continue 的作用类似于 break)throwreturn在每种情况下,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-ofyield*Array.from()Map()、Set()、WeakMap()、WeakSet()Promise.all(), Promise.race()后面的章节提供了有关关闭迭代器的更多信息。
在本节中,我们将查看更多可迭代对象的例子。大多数这些可迭代对象更容易通过生成器实现。关于生成器的章节展示了如何实现。
返回可迭代对象的工具函数和方法与可迭代数据结构一样重要。以下是用于迭代对象自身属性的工具函数。
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]] };
}
};
}
_组合器_4 是将现有可迭代对象组合起来创建新的可迭代对象的函数。
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
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']
有些可迭代对象可能永远不会_完成_。
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]
您可能会担心迭代协议速度慢,因为每次调用 next() 都会创建一个新对象。但是,在现代引擎中,小型对象的内存管理速度很快,从长远来看,引擎可以优化迭代,从而无需分配中间对象。es-discuss 上的一个帖子提供了更多信息。
原则上,没有什么可以阻止迭代器多次重复使用同一个迭代结果对象——我希望大多数情况下都能正常工作。但是,如果客户端缓存迭代结果,就会出现问题
const iterationResults = [];
const iterator = iterable[Symbol.iterator]();
let iterationResult;
while (!(iterationResult = iterator.next()).done) {
iterationResults.push(iterationResult);
}
如果迭代器重复使用其迭代结果对象,则 iterationResults 通常将多次包含同一个对象。
您可能会想知道为什么 ECMAScript 6 没有_可迭代对象组合器_,即用于处理可迭代对象或创建可迭代对象的工具。这是因为计划分两步进行
最终,一个这样的库或来自多个库的部分将被添加到 JavaScript 标准库中。
如果您想了解此类库的外观,请查看标准 Python 模块 itertools。
是的,可迭代对象很难实现——如果您手动实现它们的话。下一章将介绍_生成器_,它们可以帮助完成这项任务(以及其他任务)。
迭代协议包含以下接口(我从 Iterator 中省略了 throw(),它仅由 yield* 支持,并且是可选的)
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
return?(value? : any) : IteratorResult;
}
interface IteratorResult {
value : any;
done : boolean;
}
next() 的规则
x,next() 就会返回对象 { value: x, done: false }。next() 应始终返回一个属性 done 为 true 的对象。IteratorResult 迭代器结果的属性 done 不必是 true 或 false,真值或假值就足够了。所有内置语言机制都允许您省略 done: false。
某些可迭代对象每次被请求时都会生成一个新的迭代器。例如,数组
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,这意味着它始终返回相同的迭代器(自身)。
迭代协议区分两种完成迭代器的方式
next() 直到它返回一个属性 done 为 true 的对象。return(),您告诉迭代器您不打算再调用 next() 了。调用 return() 的规则
return() 是一个可选方法,并非所有迭代器都有。具有该方法的迭代器称为_可关闭的_。return()。例如,每当“突然”(在完成之前)离开时,for-of 都会调用 return()。以下操作会导致突然退出:break、continue(带有外部块的标签)、return、throw。实现 return() 的规则
return(x) 通常应生成对象 { done: true, value: x },但如果结果不是对象,则语言机制只会抛出错误(规范中的来源)。return() 后,next() 返回的对象也应该是 done。以下代码说明了如果在收到 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!
如果迭代器具有 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
以下类是防止迭代器被关闭的通用解决方案。它通过包装迭代器并转发除 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
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();
}
}
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()。这可能很难做到正确。
如果您使用迭代器,则应正确关闭它们。在生成器中,您可以让 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);
}
};
}
return(),则必须执行清理活动。try-finally 允许您在一个位置处理两者。return() 关闭迭代器后,它不应再通过 next() 生成任何迭代器结果。for-of 等)return 关闭迭代器,前提是(且仅当)您没有耗尽它。正确处理这一点可能很棘手。