许多编程语言都将迭代数据的方式从使用 for 循环转变到使用迭代器对象,for 循环需要初始化变量以便追踪集合内的位置,而迭代器则以编程方式返回集合中的下一个项。迭代器使操作集合变得更简单,JS 语言的很多新成分中都有迭代器的身影,如 for-of 和扩展运算符。
何为迭代器?
迭代器是被设计专用于迭代的对象,带有特定接口。所有迭代器对象都拥有 next() 方法,会返回一个结果对象。该结果对象有两个属性:对应下一个值的 value,以及一个布尔类型的 done,其值为 true 时表示没有更多值可供使用。迭代器持有一个指向集合位置的内部指针,每当调用了 next() 方法,迭代器就会返回相应的下一个值。
若你在最后一个值返回后再调用 next(),所返回的 done 属性值会是 true,并且 value 属性值会是迭代器自身的返回值(return value,即使用 return 语句明确返回的值)。该“返回值”不是原数据集的一部分,却会成为相关数据的最后一个片段,或在迭代器未提供返回值的时候使用 undefined。迭代器自身的返回值类似于函数的返回值,是向调用者返回信息的最后手段。
记住这些后,在 ES5 中创建一个迭代器就相当简单了:
1  | function createIterator (items) {  | 
正如此例演示,根据迭代器的规则来书写一个迭代器,是有一点复杂的。为此,ES6 提供了生成器,让创建迭代器对象变得更简单。
何为生成器?
生成器是能返回一个迭代器的函数。生成器由放在 function 关键字之后的一个星号(*)来表示,并能使用新的 yield 关键字。将星号紧跟在 function 关键字之后,或是在中间留出空格,都是没问题的,正如下例:
1  | function *createIterator () {  | 
生成器函数最有意思的方面可能就是它们会在每个 yield 语句后停止执行。例如,此代码中 yield 1 执行后,该函数将不会再执行任何操作,直到迭代器的 next() 方法被调用,此时才继续执行 yield 2。
yield 关键字可以和值或是表达式一起使用,因此你可以通过生成器给迭代器添加项目,而不是机械化地将项目一个个列出。
1  | function *createIterator (items) {  | 
yield 关键字只能用在生成器内部,用于其他任意位置都是语法错误,即使在生成器内部的函数中也不行!
例如下面的写法就是错误的:
1  | function *createIterator (items) {  | 
尽管 yield 严格位于 createIterator() 内部,此代码仍然有语法错误,因为 yield 无法穿越函数边界。从这点上来说,yield 与 return 非常相似,在一个被嵌套的函数中无法将值返回给包含它的函数。
生成器函数表达式
你可以使用函数表达式来创建一个生成器,只要在 function 关键字与圆括号之间使用一个星号(*)即可。例如:
1  | let createIterator = function * (items) {  | 
不能将箭头函数创建为生成器。
生成器对象方法
由于生成器就是函数,因此也可以被添加到对象中。
1  | var o = {  | 
可迭代对象和Symbol.iterator
- 首先要记住迭代器是一个对象,是一个带有特定接口、专门用来迭代的对象
 - 生成器是一个用于生成迭代器的函数
 - 只要具有 
Symbol.iterator方法的对象,就是可迭代对象 Symbol.iterator知名符号定义了为指定对象返回迭代器的函数,换句话说,Symbol.iterator方法是可迭代对象的生成器方法- 生成器创建的所有迭代器都是可迭代对象,因为生成器默认就会为 
Symbol.iterator属性赋值 
在 ES6 中,所有的集合对象(数组、Set和Map)以及字符串都是可迭代对象,因此它们都被指定了默认的迭代器。
for-of循环
for-of 首先会调用可迭代对象的 Symbol.iterator 来生成迭代器,然后在循环中调用迭代器的 next() 方法,并将迭代器的 value 值存储在一个变量上,循环过程会持续到迭代器的 done 属性变为 true 为止。
访问默认迭代器
你可以使用 Symbol.iterator 来访问对象上的默认迭代器,就像这样:
1  | let values = [1, 2, 3]  | 
既然 Symbol.iterator 指定了默认迭代器,你就可以使用它来检测一个对象是否能进行迭代,正如下例:
1  | function isIterable (object) {  | 
这个 isIterable() 函数仅仅查看对象是否存在一个类型为函数的默认迭代器。for-of 循环在执行之前会做类似的检查。
创建可迭代对象
开发者自定义对象默认情况下不是可迭代对象,但你可以创建一个包含生成器的 Symbol.iterator 属性,让它们成为可迭代对象。例如:
1  | let collection = {  | 
内置的迭代器
集合的迭代器
ES6 具有三种集合对象类型:数组、Map 和 Set。这三种类型都拥有如下迭代器,有助于探索它们的内容:
entries():返回一个包含键值对的迭代器。values():返回一个包含集合中的值的迭代器。keys():返回一个包含集合中的键的迭代器。
entries
entries() 迭代器会在每次 next() 被调用时返回一个双项数组,此数组代表了集合中每个元素的键与值:对于数组来说,第一项是数值索引。对于 Set,第一项也是值。对于 Map,第一项就是键。
记住,第一项是键,第二项才是值,别用混了……
1  | let colors = [ 'red', 'green', 'blue' ]  | 
values
values() 迭代器仅仅能返回存储在集合内的值。
1  | let colors = [ 'red', 'green', 'blue' ]  | 
keys
keys() 迭代器能返回集合中的每一个键。对于数组来说,它只返回了数值类型的键,永不返回数组的其他自有属性。Set 的键与值时相同的,因此它的 keys() 与 values() 返回了相同的迭代器。对于 Map,keys() 迭代器返回了每个不重复的键。
1  | let colors = [ 'red', 'green', 'blue' ]  | 
集合类型的默认迭代器
当 for-of 循环没有显示指定迭代器时,每种集合类型都有一个默认的迭代器供循环使用。values() 方法是数组与 Set 的默认迭代器,而 entries() 方法则是 Map 的默认迭代器。
Map 默认迭代器的行为有助于在 for-of 循环中使用解构,正如此例:
1  | let data = new Map()  | 
字符串和 NodeList
需要记住,可以对字符串和 NodeList 使用 for-of循环!
扩展运算符与非数组的可迭代对象
扩展运算符能作用于所有可迭代对象,并且会使用默认迭代器来判断需要使用哪些值。所有的值都从迭代器中被读取出来并插入数组,遵循迭代器返回值的顺序。
1  | let mySet = new Set([1, 2, 3, 3, 3, 4, 5])  | 
迭代器高级功能
传递参数给迭代器
你可以通过 next() 方法向迭代器传递参数。当一个参数被传递给 next() 方法时,该参数就会成为生成器内部 yield 语句的值。此处有个基本范例:
1  | function *createIterator () {  | 
对于 next() 的首次调用是一个特殊情况,传给它的任意参数都会被忽略。由于传递给 next() 的参数会成为 yield 语句的值,该 yield 语句指的是上次生成器中断执行处的语句。而 next() 方法第一次被调用时,生成器函数才刚刚开始执行,没有所谓的“上一次中断处的 yield 语句”可供赋值。因此在第一次调用 next() 时,不存在任何向其传递参数的理由。
在第二次调用 next() 时,4 作为参数被传递进去,这个 4 最终被赋值给了生成器函数内部的 first 变量。在包含赋值操作的第一个 yield 语句中,表达式右侧在第一次调用 next() 时被计算,而表达式左侧则在第二次调用 next() 方法、并在生成器函数继续执行前被计算。由于第二次调用 next() 传入了 4,这个值就被赋给了 first 变量,之后生成器继续执行。
第二个 yield 使用了第一个 yield 的结果并加上了 2,也就是返回了一个 6。当 next() 被第三次调用时,传入了参数 5。这个值被赋给了 second 变量,并随后用在了第三个 yield 语句中,返回了 8。
在迭代器中抛出错误
能传递给迭代器的不仅是数据,还可以是错误条件。迭代器可以选择实现一个 throw() 方法,用于指示迭代器应在恢复执行时抛出一个错误。这是对异步编程来说很重要的一个能力,同时也会增加生成器内部的灵活度,能够既模仿返回一个值,又模仿抛出错误。你可以传递一个错误对象给 throw() 方法,当迭代器继续进行处理时应当抛出此错误。例如:
如果你不使用 throw() 传递一个错误给迭代器,而是通过 next() 传递一个错误给迭代器,那么迭代器内部不会抛出错误!
1  | function *createIterator () {  | 
了解这些之后,你就可以在生成器内部使用一个 try-catch 块来捕捉这种错误:
1  | function *createIterator () {  | 
本例使用一个 try-catch 块包裹了第二个 yield 语句。尽管这个 yield 自身的执行不会出错,但在对 second 变量赋值之前,错误就在此时被抛出,于是 catch 部分捕捉错误并将这个变量赋值为 6,然后再继续执行到下一个 yield 处并返回了 9。
要注意一件有趣的事情发生了:throw() 方法就像 next() 方法一样返回了一个结果对象。由于错误在生成器内部被捕捉,代码继续执行到下一个 yield 处并返回了下一个值,也就是 9。
将 next() 与 throw() 都当作迭代器的指令,会有助于思考。next() 方法指示迭代器继续执行,而 throw() 方法则指示迭代器通过抛出一个错误继续执行。
生成器的 return
由于生成器是函数,你可以在它内部使用 return 语句,既可以让生成器早一点退出执行,也可以指定在 next() 方法最后一次调用时的返回值。在生成器内,return 表明所有的处理已完成,因此 done 属性会被设为 true,而如果提供了返回值,就会被用于 value 字段。此处有个例子,单纯使用 return 让生成器更早返回:
1  | function *createIterator () {  | 
你也可以指定一个返回值,会被用于最终返回的结果对象中的 value 字段。例如:
1  | function *createIterator () {  | 
扩展运算符与 for-of 循环会忽略 return 语句所指定的任意值。一旦它们看到 done 的值为 true,它们就会停止操作而不会读取对应的 value 值。
生成器委托
在某些情况下,将两个迭代器的值合并一起会更有用。生成器可以用星号(*)配合 yield 这一特殊形式来委托其他的迭代器。正如生成器的定义,星号出现在何处是不重要的,只要落在 yield 关键字与生成器函数名之间即可。此处有个范例:
1  | function *createNumberIterator () {  | 
此例中的 createCombinedIterator() 生成器依次委托了 createNumberIterator() 与 createColorIterator()。返回的迭代器从外部看来就是一个单一的迭代器,用于产生所有的值。每次对 next() 的调用都会委托给合适的生成器,直到使用 createNumberIterator() 与 createColorIterator() 创建的迭代器全部清空为止。然后最终的 yield 会被执行以返回true。
生成器委托也能让你进一步使用生成器的返回值。这是访问这些返回值的最简单方式,并且在执行复杂任务时会非常有用。例如:
1  | function *createNumberIterator () {  | 
注意观察,第三次调用 next() 方法时,代码在 createNumberIterator() 内的 return 语句处并没有停止执行,而是直接运行到 createCombinedIterator() 内的下一个 yield 处才停止执行的。
此处 createCombinedIterator() 生成器委托了 createNumberIterator() 并将它的返回值赋值给了 result 变量。由于 createNumberIterator() 包含 return 3 语句,该返回值就是 3。result 变量接下来会作为参数传递给 createRepeatingIterator() 生成器,指示同一个字符串需要被重复几次。
你也可以直接在字符串上使用 yield *,字符串的默认迭代器会被使用。
1  | function *createCombinedIterator () {  | 
异步任务运行
执行异步操作的传统方式是调用一个包含回调的函数。例如,在 Node 中从磁盘读取一个文件:
1  | let fs = require('fs')  | 
当你拥有数量少而有限的任务需要完成时,这么做很有效。然而当你需要嵌套回调函数,或者要按顺序处理一系列的异步任务时,传统方式就会非常麻烦。在这种场合下,生成器与 yield 会很有用。
一个简单的任务运行器
由于 yield 能停止运行,并在重新运行前等待 next() 方法被调用,你就可以在没有回调函数的情况下实现异步调用。首先,你需要一个能够调用生成器并启动迭代器的函数,就像这样:
1  | function run (taskDef) {  | 
配合这个已实现的 run() 函数,你就可以运行一个包含多条 yield 语句的生成器,就像这样:
1  | run (function * () {  | 
此例只是将三个数值输出到控制台,单纯用于表明对 next() 的所有调用都已被执行。然而,仅仅使用几次 yield 并不太有意义,下一步是要把值传进迭代器并获取返回数据。
带数据的任务运行
传递数据给任务运行器最简单的方式,就是把 yield 返回的值传入下一次的 next() 调用。为此,你仅需传递 result.value,正如以下代码:
1  | function run (taskDef) {  | 
异步任务运行器
下面的代码是一个使用生成器来运行异步任务的例子。
ps. 为了保持纯粹,这个例子中并没有使用 Promise
1  | let fs = require('fs')  |