Iterables和迭代器
栏目: JavaScript · 发布时间: 7年前
内容简介:ES6引入了一种遍历数据的新机制:迭代。两个概念是迭代的核心:在TypeScript 中为接口表示方式,如下所示:下面几种:
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 和迭代器 是所谓的迭代协议( 接口加上使用它们的规则 )的一部分。该协议的一个关键特征是它是顺序的:迭代器每次返回一个值。这意味着,如果可迭代数据结构是非线性的(如树),迭代将使其线性化。
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)
}
}
}
复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 迭代器萃取与反向迭代器
- 浅谈python可迭代对象,迭代器
- 可迭代对象,迭代器(对象),生成器(对象)
- 终于把动态规划与策略迭代、值迭代讲清楚了
- 终于把动态规划与策略迭代、值迭代讲清楚了
- 搞清楚 Python 的迭代器、可迭代对象、生成器
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Where Wizards Stay Up Late
Katie Hafner / Simon & Schuster / 1998-1-21 / USD 16.00
Twenty five years ago, it didn't exist. Today, twenty million people worldwide are surfing the Net. "Where Wizards Stay Up Late" is the exciting story of the pioneers responsible for creating the most......一起来看看 《Where Wizards Stay Up Late》 这本书的介绍吧!