1 理解迭代
在 JavaScript 中,计数循环就是一种简单的迭代
循环是迭代机制的基础,因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的
迭代会在一个有序集合上进行,"有序"可以理解为集合所有项都可以按照既定的顺序被遍历到,数组就是 JavaScript 中有序集合的最典型例子
通过这样的循环来执行存在以下问题:
1)迭代之前需要事先知道如何使用数据结构。数组中的每一项都只能先通过引用取得数组对象,通过 [] 操作符取得特定索引位置上的项,这种情况并不适用于所有数据结构
2)遍历顺序并不是数据结构固有的。通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构
ES5新增了 Array.prototype.forEach() 方法,这解决了单独记录索引和通过数组对象取得值的问题,但没办法标识迭代何时终止,因此这只适用于数组
很多语言通过原生语言结构解决了这个问题,我们无须事先知道如何迭代就能实现迭代操作,这个解决方案就是迭代器模式,ES6也支持了迭代器模式
2 迭代器模式
迭代器模式把有些结构称为"可迭代对象",因为它们实现了正式的 Iterable 接口,可以通过迭代器来逐一访问其中的元素
可迭代对象并不仅仅是数组或集合,它指的是任何可以通过某种遍历机制访问其元素的数据结构,这些元素是有限的,并且具有明确的遍历顺序,比如最开始提到的计数循环。该循环中生成的值是暂时性的,但循环本身是在执行迭代,计数循环和数组都具有可迭代对象的行为
PS:临时性可迭代对象可以实现为生成器
2.1 可迭代协议
实现 Iterable 接口(可迭代协议)意味着必须暴露一个属性作为"默认迭代器",而且这个属性必须使用特殊的 Symbol.iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器
很多内置类型都实现了 Iterable 接口:字符串、数组、映射、集合、arguments对象、NodeList等 DOM 集合类型
实际写代码过程中,不需要显式调用这个工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性,接收可迭代对象的原生语言特性包括:for-of循环、数组解构、扩展操作符、Array.from()、创建集合、创建映射、Promise.all()/race()、yield操作符,这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器
如果对象原型链上的父类实现了 Iterable 接口,那这个对象也就实现了这个接口
2.2 迭代器协议
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next() 方法在可迭代对象中遍历数据,每次成功调用next(),都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值;若不调用,则无法知道迭代器的当前位置
IteratorResult 对象包含两个属性:done 和 value
- done是一个布尔值,表示是否还可以再次调用 next() 取得下一个值
- value包含可迭代对象的下一个值(done为false),或者undefined(done为true)
迭代器并不知道怎么从可迭代对象中取得下一个值,也不知道可迭代对象有多大,只要迭代器到达 done: true 状态,后续调用 next() 就一直返回undefined
每个迭代器都表示对可迭代对象的一次性有序遍历,不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象
迭代器仅仅是使用游标来记录遍历可迭代对象的历程,如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化
PS:迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象
2.3 提前终止迭代器
可选的 return() 方法用于指定在迭代器提前关闭时执行的逻辑,执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以"关闭"迭代器,可能的情况包括:
- for-of循环通过 break、continue、return或throw 提前退出
- 解构操作并未消费所有值
return()方法必须返回一个有效的 IteratorResult 对象,简单来说,可以只返回 done: true,因为这个返回值只会用在生成器的上下文中
如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代,比如数组的迭代器就是不能关闭的
因为 return() 方法是可选的,所以并非所有迭代器都是可关闭的。要知道某个迭代器是否可关闭,可以测试这个迭代器实例的 return 属性是不是函数对象。不过,仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的,因为调用 return() 不会强制迭代器进入关闭状态
3 生成器
3.1 生成器基础
生成器的形式是一个函数,函数名称前面加一个星号*,表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。标示生成器函数的星号不受两侧空格的影响
PS:箭头函数不能用来定义生成器函数
调用生成器函数会产生一个生成器对象,它一开始处于暂停执行的状态,与迭代器相似,生成器对象也实现了 Iterator 接口,也具有 next() 方法,调用这个方法会让生成器开始或恢复执行
next() 方法的返回值类似于迭代器,有 done、value属性,函数体为空的生成器函数中间不会停留,调用一次 next() 就会让生成器到达 done: true 状态
value属性是生成器函数的返回值,默认值为undefined,可以通过生成器函数的返回值指定
生成函数只会在初次调用 next() 方法后开始执行,如下所示:
3.2 通过yield中断执行
生成器在遇到 yield 关键字之前会正常执行,遇到后执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next() 来恢复执行
此时的 yield 关键字有点像函数的中间返回语句,它生成的值会出现在 next() 方法返回的对象里。通过 yield 退出的生成器函数会处在 done: false 状态,通过 return 退出的生成器函数会处在 done: true 状态
yield只能在生成器函数内部使用,用在其他地方会抛出错误。类似函数的 return,yield必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误
1)生成器对象作为可迭代对象
在需要自定义迭代对象时,把生成器对象当作可迭代对象,这样使用生成器对象会特别有用。比如:我们需要定义一个可迭代对象,而它会产生一个迭代器,这个迭代器会执行指定的次数。使用生成器,可以通过一个简单的循环来实现:
传给生成器的函数可以控制迭代次数,在 n 为 0 时,while条件为假,循环退出,生成器函数返回
2)使用 yield 实现输入输出
yield还可以作为函数的中间参数使用,上一次让生成器函数暂停的 yield 会接收到传给 next() 方法的第一个值
PS:第一次调用 next() 传入的值不会被使用,因为这一次调用是为了开始执行生成器函数
yield可以同时用于输入和输出
因为函数必须对整个表达式求值才能确定要返回的值,所以在遇到 yield 时暂停执行并计算出要产生的值: foo;下一次调用 next() 传入了bar,作为交给同一个 yield 的值,这个值被确定为本次生成器函数要返回的值
yield并非只能使用一次,假设我们想定义一个生成器函数,它会根据配置的值迭代相应次数并产生迭代的索引
3) 产生可迭代对象
可以使用星号增强 yield 的行为,让它能够迭代一个可迭代对象,从而一次产出一个值
3.3 生成器作为默认迭代器
因为生成器对象实现了 Iterable 接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器很适合作为默认迭代器。下面是一个简单的例子,这个类的默认迭代器可以用一行代码产出类的内容:
for-of循环调用了默认迭代器(它恰好又是一个生成器函数)并产生了一个生成器对象,这个生成器对象是可迭代的,所以完全可以在迭代中使用
3.4 提前终止生成器
与迭代器类似,生成器也支持"可关闭",生成器对象除了之前迭代器那两种方法,还有第三种方法:throw(),return() 和 throw() 都可以用于强制生成器进入关闭状态
1) return()
提供给 return() 方法的值,就是终止迭代器对象的值。与迭代器不同,所有生成器对象都有 return() 方法,只要通过它进入关闭状态,就无法恢复了。后续调用 next() 会显示 done: true 状态,而提供的任何返回值都不会被存储或传播
2) throw()
throw()会在暂停的时候将一个提供的错误注入到生成器对象中,如果错误未被处理,生成器就会关闭
假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行,错误处理会跳过对应的yield
在上面这段代码中,生成器在 try/catch 块中的 yield 关键字处暂停执行。在暂停期间,throw() 方法向生成器对象内部注入了一个错误,这个错误会被 yield 抛出。由于 yield 抛出了这个错误,生成器就不会再产出值2,此时生成器继续执行,在下一次迭代再次遇到 yield 时产出了值3
PS:如果生成器对象还没有开始执行,那么调用 throw() 抛出的错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误