企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] # 21 Iterables 和迭代器 ## 21.1 概述 ES6引入了一种遍历数据的新机制:迭代。两个概念是迭代的核心: 1. 可迭代是一种数据结构,让我们可以方便的访问其元素。它通过实现一个键为 `Symbol.iterator` 的方法来实现。这是迭代器的工厂方法。 2. 迭代器是用于遍历数据结构元素的指针(想想数据库中的游标 (cursors ))。 如下是在 TypeScript 中用接口来表示的方式: ```ts interface Iterable { [Symbol.iterator]() : Iterator; } interface Iterator { next() : IteratorResult; } interface IteratorResult { value: any; done: boolean; } ``` ### 21.1.1 可迭代的对象 **可以供 for...of 消费的原生数据结构** * Array * Map * Set * String * **TypedArray(一种通用的固定长度缓冲区类型,允许读取缓冲区中的二进制数据)** * 函数中的 arguments 对象 * NodeList 对象 为什么原生数据结构中并没有对象(Object)? 字面量对象是不可迭代的,具体下面会有相关介绍。 那是因为对象属性的遍历先后顺序是不确定的,需要开发者手动指定。**本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口就等于部署一种线性变换**。 ### 21.1.2 内部构造使用 Iterator 接口 * 通过数组模式进行解构: ```js const [ a , b ] = new Set ([ 'a' , 'b' , 'c' ]); ``` * `for-of`循环: ```js for ( const [ 'a' , 'b' , 'c' ]) { console . log ( x ); } ``` * `Array.from()` : ```js const arr = Array . from ( new Set ([ 'a' , 'b' , 'c' ])); ``` * 展开运算符( `...` ): ```js const arr = [... new Set ([ 'a' , 'b' , 'c' ])]; ``` * Maps 和Sets 的构造器: ```js const map = new Map ([[ false , 'no' ], [ true , 'yes' ]]); const set = new Set ([ 'a' , 'b' , 'c' ]); ``` * `Promise.all()` ,`Promise.race()`: ```js Promise . all ( iterableOverPromises ). then ( ··· ); Promise . race ( iterableOverPromises ). then ( ··· ); ``` * `yield*` : ```js yield * anIterable ; ``` ## 21.2 可迭代性 可迭代性的主要概念如下: * Data consumers(数据消费者):JavaScript具有使用数据的语言结构。例如,`for-of` 循环遍历值,而 spread 操作符(`…`)将值插入数组或函数调用中。 * Data sources(数据源):数据消费者可以从各种数据源获取其值。例如,您可能希望迭代数组的元素、Map 中的键值条目或字符串的字符。 每个消费者都支持所有来源是不切实际的,特别是因为可以创建新的来源(例如通过库)。 需要一种统一的接口机制,来处理所有不同的数据结构。 因此,ES6引入了`Iterable` 。 数据消费者使用它,数据源实现它: ![](https://box.kancloud.cn/e634d8d6170a1304e1d65adb591ce1aa_1287x433.jpg =340x120) 因为JS中没有接口,所以遍历器(Iterator)更像是一种约定。为各种不同的数据结构提供统一的访问机制。 任何数据结构只要部署了 Iterator 接口,我们就成这种数据结构为 “可遍历”(Iterable)。**ES6 规定,默认的 Iterator 接口部署在数据结构的 `Symbol.iterator` 属性,或者说,一个数据结构只要具有 `Symbol.iterator` 数据,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 * Source(源):如果某个值的方法的键是符号`Symbol.iterator`,它返回一个所谓的迭代器(iterator),则该值被认为是可迭代的(iterable)。 迭代器是一个通过其方法 `next()`返回值的对象。 我们说:它迭代可迭代的项(内容),每次调用每次返回一个值。 * Consumption (消费):数据消费者使用迭代器检索他们正在使用的值。 现在来看看,数组`arr` 可以如何消费?首先通过 键为`Symbol.iterator`的方法,创建一个迭代器: ```js const arr = ['a', 'b', 'c']; const iter = arr[Symbol.iterator](); ``` 然后通过该迭代器的 `next()` 方法重复检索 该数组中的每个项: ```js > 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 和迭代器 是所谓的迭代协议([接口加上使用它们的规则](http://exploringjs.com/es6/ch_about-book.html#sec_protocol))的一部分。该协议的一个关键特征是它是顺序的:迭代器每次返回一个值。这意味着,如果可迭代数据结构是非线性的(如树),迭代将使其线性化。 ## 21.3 可迭代数据源 我将使用`for-of`循环(参见章节 [for-of循环](http://exploringjs.com/es6/ch_for-of.html#ch_for-of))迭代各种可迭代数据。 ### 21.3.1 数组 数组(和Typed Arrays)可迭代其元素: ```js 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数据结构最终都是可迭代的: ```js 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元素及其索引的好方法: ```js const arr = ['a', 'b', 'c']; for (const pair of arr.entries()) { console.log(pair); } // Output: // [0, 'a'] // [1, 'b'] // [2, 'c'] ``` ### 21.3.8 普通对象不可迭代 普通对象(由对象字面量创建)不可迭代: ```js for (const x of {}) { // TypeError console.log(x); } ``` 默认情况下,为什么对象不能在属性上迭代? 推理如下。 您可以在JavaScript中迭代两个级别: * 程序级:迭代属性意味着检查程序的结构。 * 数据级别:迭代数据结构意味着检查程序管理的数据。 对属性进行迭代默认意味着混合这些级别,这将有两个缺点: * 您无法迭代数据结构的属性。 * 迭代对象的属性后,将该对象转换为数据结构会破坏您的代码。 如果引擎要通过方法 `Object.prototype[Symbol.iterator]()` 实现迭代,那么还会有一个警告:通过 `Object.create(null)` 创建的对象将不可迭代,因为`Object.prototype`不在他们的原型链。 重要的是要记住,如果将[objects 用作Maps](http://exploringjs.com/es6/leanpub-endnotes.html#fn-iteration_1),则迭代对象的属性大多是有趣的。 但我们只在ES5中这样做,那时我们没有更好的选择。 在ECMAScript 6中,我们有内置的数据结构 `Map`。 #### 21.3.8.1 如何迭代属性 迭代属性的正确(和安全)方法是通过工具函数。 例如,通过 `objectEntries()`, 它的实现将在后面显示(未来的ECMAScript版本可能内置了类似的东西): ```js 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 通过数组模式进行解构 通过数组模式进行解构适用于任何可迭代: ```js 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循环](http://exploringjs.com/es6/ch_for-of.html#ch_for-of)。 请注意,iterable 的 可迭代性是必需的,否则 `for-of` 不能循环值。 这意味着必须将非可迭代值转换为可迭代的值。 例如,通过`Array.from()`。 ### 21.4.3 `Array.from()` `Array.from()`将可迭代和类似 Array 的值转换为 Arrays。 它也适用于typed Arrays。 ```js > 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()`更多信息,请参阅有关[数组的章节](http://exploringjs.com/es6/ch_arrays.html#Array_from) 。 ### 21.4.4 展开运算符( ... ) spread运算符将iterable的值插入到Array中: ```js > const arr = ['b', 'c']; > ['a', ...arr, 'd'] ['a', 'b', 'c', 'd'] ``` 这意味着它为您提供了一种将任何迭代转换为数组的简便方式: ```js const arr = [... iterable ]; ``` 展开运算符还将 iterable 转换为函数,方法或构造函数调用的参数: ```js > Math.max(...[-1, 8, 3]) 8 ``` ### 21.4.5 Maps 和Sets 构造函数 Map的构造函数将 [key,value] 对上的可迭代变为Map: ```js > const map = new Map([['uno', 'one'], ['dos', 'two']]); > map.get('uno') 'one' > map.get('dos') 'two' ``` Set的构造函数将可迭代的元素转换为Set: ```js > 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上的迭代: ```js Promise.all(iterableOverPromises).then(···); Promise.race(iterableOverPromises).then(···); ``` ### 21.4.7 `yield*` `yield*` 是仅在生成器内可用的运算符。 它产生迭代对象所迭代的所有项。 ```js function* yieldAllValuesOf(iterable) { yield* iterable; } ``` `yield*` 最重要的用例是递归调用生成器(生成可迭代的东西)。 ## 21.5 实现迭代 在本节中,我将详细解释如何实现iterables。请注意,[ES6生成器](http://exploringjs.com/es6/ch_generators.html#ch_generators)通常比“手动”更方便去实现。 迭代协议如下所示: ![](https://box.kancloud.cn/2d2d447adb878f48229334be57ab8726_1683x370.jpg =420x100) 如果对象具有其键为`Symbol.iterator`的方法(自己的或继承的),则该对象变为可迭代 (“实现” Iterable )。 该方法必须返回一个迭代器 ,一个通过其方法`next()`迭代的“内部” 项的对象。 在 TypeScript 表示中,iterables 和迭代器的接口如下所示: ```js interface Iterable { [Symbol.iterator]() : Iterator; } interface Iterator { next() : IteratorResult; return?(value? : any) : IteratorResult; } interface IteratorResult { value: any; done: boolean; } ``` `return()` 是一个可选的方法,我们将在[以后使用](http://exploringjs.com/es6/leanpub-endnotes.html#fn-iteration_3)。让我们首先实现一个模拟的迭代,以了解迭代的工作原理。 ```js 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` 实际上是可迭代的: ```js for (const x of iterable) { console.log(x) } // Output: // hello // world ``` 代码执行三个步骤,计数器 step 确保一切都以正确的顺序发生。 首先,我们返回值'hello' ,然后返回值'world' ,然后我们指示已经迭代结束。 每个项目都包含在一个具有以下属性的对象中: - `value` 保存实际值 - `done` 是一个布尔标志,指示是否已到达终点 如果为`false`,则可以省略`done`;如果`undefined`,则可以省略`value`。 也就是说,`switch`语句可以写成如下。 ```js switch (step) { case 1: return { value: 'hello' } case 2: return { value: 'world' } default: return { done: true } } ``` 正如 生成器的章节中所解释的那样,在某些情况下,您甚至需要最后一项`done: true`才能获得 value。 否则,`next()`可以更简单并直接返回项目(不将它们包装在对象中)。 然后通过特殊值(例如,一个 symbol)指示迭代的结束。 让我们再看一个可迭代的实现。 函数 `iterateOver()` 在通过传递给它的参数,返回一个 iterable: ```js 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 可迭代的迭代器 如果可迭代对象 和迭代器是同一个对象,则可以简化前一个函数: ```js 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 和迭代器不是同一个对象,如果迭代器具有以下方法(这也使它成为可迭代的),它偶尔会有用: ```js [Symbol.iterator]() { return this; } ``` 所有内置的 ES6 迭代器都遵循这种模式(通过一个通用的原型,参见有关生成器的章节 )。 例如,Arrays 的默认迭代器: ```js > const arr = []; > const iterator = arr[Symbol.iterator](); > iterator[Symbol.iterator]() === iterator > true ``` 如果迭代器也是可迭代的,那么它有什么用呢? `for-of` 仅适用于 iterables,不适用于迭代器。 因为 Array 迭代器是可迭代的,所以可以在另一个循环中继续迭代: ```js 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*` 迭代的生成器。 有关[生成器的章节](http://exploringjs.com/es6/ch_generators.html#ch_generators)对此进行了解释。 #### 21.5.2.1 通过 `return()` 关闭迭代器 如前所述,可选的迭代器方法 `return()` 是关于如果迭代器没有迭代直到结束 而让迭代器清理的。 它关闭了一个迭代器。 在 for-of 循环中,过早(或突然 ,在规范语言中)终止可能由以下原因引起: - break - continue (如果您继续外部循环,则 `continue` 的作用类似于 `break`) - throw - return 在每种情况下, `for-of` 让迭代器知道循环不会完成。 让我们看一个例子,一个函数 `readLinesSync` ,它返回一个文件中的可迭代文本行,并且无论发生什么都想关闭该文件: ```js function readLinesSync(fileName) { const file = ···; return { ··· next() { if (file.isAtEndOfFile()) { file.close(); return { done: true }; } ··· }, return() { file.close(); return { done: true }; }, }; } ``` 由于 `return()`,文件将在以下循环中正确关闭: ```js // Only print first line for (const line of readLinesSync(fileName)) { console.log(x) break } ``` `return()`方法必须返回一个对象。这是由于生成器处理 `return` 语句的方式造成的,有关[生成器的章节](http://exploringjs.com/es6/ch_generators.html#ch_generators)将对此进行解释。 以下构造关闭未完全“耗尽”的迭代器: - for-of - yield\* - Destructuring - Array.from() - Map(), Set(), WeakMap(), WeakSet() - Promise.all(), Promise.race() [稍后的部分](http://exploringjs.com/es6/ch_iteration.html#sec_closing-iterators)将提供关于关闭迭代器的更多信息。 ## 21.6 可迭代的更多例子 在本节中,我们将看一些可迭代的例子。 大多数这些迭代更容易通过生成器实现。关于[生成器的一章](http://exploringjs.com/es6/ch_generators.html#ch_generators)展示了如何实现。 ### 21.6.1 返回 iterables 的工具函数 返回可迭代的工具函数和方法与可迭代数据结构一样重要。 以下是用于迭代对象的自身属性的工具函数。 ```js 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 ``` 另一种选择是使用迭代器而不是索引来遍历具有属性键的数组: ```js 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 迭代的组合器 [组合器](http://exploringjs.com/es6/leanpub-endnotes.html#fn-iteration_4) 是组合现有迭代(iterables)来创建新迭代的函数。 #### 21.6.2.1 `take(n, iterable)` 让我们从组合函数 `take(n, iterable)`,它返回可迭代的前 `n` 项的 `iterable`。 ```js 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` 的数组)的可迭代项。。 ```js 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` 决定了结果的长度: ```js 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 。 ```js function naturalNumbers() { let n = 0 return { [Symbol.iterator]() { return this }, next() { return { value: n++ } } } } ``` 对于无限迭代,您一定不要去遍历它的所有项。例如,通过从 `for-of` 循环中断开 ```js for (const x of naturalNumbers()) { if (x > 2) break //这里进行中断 console.log(x) } ``` 或者只访问无限可迭代的开头: ```js const [a, b, c] = naturalNumbers() // a=0; b=1; c=2; ``` 或者使用组合器。`take()` 是一种可能性: ```js for (const x of take(3, naturalNumbers())) { console.log(x) } // Output: // 0 // 1 // 2 ``` `zip()`返回的 iterable 的“长度”由其最短的输入可迭代决定。 这意味着 `zip()` 和 `naturalNumbers()` 为您提供了对任意(有限)长度的迭代器进行编号的方法: ```js 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 上的[一个帖子](https://esdiscuss.org/topic/performance-of-iterator-next-as-specified)有更多的信息。 ### 21.7.2 我可以多次重复使用同一个对象吗? 原则上,没有什么能阻止迭代器多次重复使用相同的迭代结果对象 - 我希望大多数事情都能正常工作。 但是,如果客户端缓存迭代结果,则会出现问题: ```js 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)很难实现吗? 是的,迭代很难实现 - 如果你手动去实现它们。 下一章将介绍有助于完成此任务的[生成器](http://exploringjs.com/es6/ch_generators.html#ch_generators) (以及其他内容)。 # 21.8 深入的 ECMAScript 6 迭代协议 迭代协议包含以下接口(我省略了 Iterator 中的 `throw()`,它只受 `yield*` 支持,并且是可选的): ```js interface Iterable { [Symbol.iterator]() : Iterator; } interface Iterator { next() : IteratorResult; return?(value? : any) : IteratorResult; } interface IteratorResult { value : any; done : boolean; } ``` 规范有一个[关于迭代协议的部分](http://www.ecma-international.org/ecma-262/6.0/#sec-iteration) 。 ## 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 每次被要求生成一个新的迭代器。 例如,数组: ```js function getIterator(iterable) { return iterable[Symbol.iterator]() } const iterable = ['a', 'b'] console.log(getIterator(iterable) === getIterator(iterable)) // false ``` 其他迭代每次都返回相同的迭代器。 例如,生成器对象: ```js function* elements() { yield 'a' yield 'b' } const iterable = elements() console.log(getIterator(iterable) === getIterator(iterable)) // true ``` 当您多次迭代同一迭代器时,可迭代(iterable)是否产生新的迭代器并不重要。例如,通过以下函数: ```js function iterateTwice(iterable) { for (const x of iterable) { console.log(x) } for (const x of iterable) { console.log(x) } } ``` 使用新的迭代器,您可以多次迭代相同的可迭代: ```js iterateTwice(['a', 'b']) // Output: // a // b // a // b ``` 如果每次都返回相同的迭代器,则不能: ```js 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](http://www.ecma-international.org/ecma-262/6.0/#sec-iteratorclose))。 - 调用 `return()`后,`next()` 返回的对象也应该 `done`。 下面的代码说明了 `for-of` 循环 如果在收到 一个 `done` 迭代器结果 之前中止它,则它调用 `return()`。 也就是说,如果在收到最后一个值后中止,则甚至会调用 return() 。 这是微妙的,当您手动迭代或实现迭代器时,您必须小心谨慎。 ```js 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 迭代器不是: ```js > let iterable = ['a', 'b', 'c']; > const iterator = iterable[Symbol.iterator](); > 'return' in iterator false ``` 默认情况下,Generator 对象是可关闭的。 例如,由以下生成器函数返回的: ```js function* elements() { yield 'a' yield 'b' yield 'c' } ``` 如果在 `elements()`的结果上调用 `return()`,则迭代完成: ```js > const iterator = elements(); > iterator.next() { value: 'a', done: false } > iterator.return() { value: undefined, done: true } > iterator.next() { value: undefined, done: true } ``` 如果迭代器不可关闭,则可以在 `for-of` 循环中突然退出(例如 A 行中的那个)之后继续迭代它: ```js 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()`内的第二个循环没有任何可迭代的东西: ```js 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()`之外的所有方法调用来实现这一点。 ```js 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()` 的结果将不会被关闭。 ```js 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` 的原型中),如下所示: ```js // 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` 对生成器进行清理 一些生成器需要在迭代完成后清理(释放已分配的资源,关闭打开的文件等)。这就是我们实现它的方式: ```js function* genFunc() { yield 'a' yield 'b' console.log('Performing cleanup') } ``` 在正常的 for-of 循环中,一切都很好: ```js for (const x of genFunc()) { console.log(x) } // Output: // a // b // Performing cleanup ``` 但是,如果在第一次 yield 后退出循环,则执行似乎永远停留在那里并且永远不会到达清理步骤: ```js for (const x of genFunc()) { console.log(x) break } // Output: // a ``` 实际发生的情况是,每当提前离开 `for-of` 循环时,`for-of` 都会向当前迭代器发送 `return()`。这意味着没有完成清理步骤,因为生成器函数就提前返回。 值得庆幸的是,通过在 `finally` 子句中执行清理可以很容易地解决这个问题: ```js function* genFunc() { try { yield 'a' yield 'b' } finally { console.log('Performing cleanup') } } ``` 现在一切都按预期工作: ```js for (const x of genFunc()) { console.log(x) break } // Output: // a // Performing cleanup ``` 因此,使用需要以某种方式关闭或清理的资源的一般模式是: ```js function* funcThatUsesResource() { const resource = allocateResource(); try { ··· } finally { resource.deallocate(); } } ``` #### 21.8.2.4 在手动实现的迭代器中处理清理 ```js 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 所有工作为您完成: ```js /** * 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 } } ``` 如果您手动去管理,需要做一些工作: ```js 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() } } ``` 如果不使用生成器,则需要做更多工作: ```js 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` 关闭迭代器,当且仅当您没有耗尽它时。要做到这一点可能很棘手。 - 在突然退出后继续在迭代器上迭代:迭代器必须是不可关闭的或不可关闭的(例如通过工具类)。 > [ES6 迭代器 (Iterator) 和 for...of 循环使用方法]([https://www.jianshu.com/p/3bb77516fa7e](https://www.jianshu.com/p/3bb77516fa7e))