迭代器与生成器

理解迭代

在软件开发领域,“迭代”的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件

迭代就是循环执行,循环是迭代机制的基础,且迭代会在一个有序集合上进行。

在无迭代器时,迭代有两个缺点

  • 迭代之前需要事先知道如何使用数据结构
  • 遍历顺序并不是数据结构固有的

forEach()方法虽然解决单独记录索引和通过数组对象取得值的问题。不过,没有办法标识迭代何时终止

迭代器模式

开发者无须事先知道如何迭代就能实现迭代操作。

迭代器模式描述了一个方案,即可以把有些结构称为“可迭代对象”(iterable),因为它们实现了正式的Iterable接口,而且可以通过迭代器Iterator消费

迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现Iterable接口的对象都有一个Symbol.iterator属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器工厂,也就是一个函数,调用之后会产生一个实现Iterator接口的对象。

可迭代对象

  • 包含元素有限
  • 具有无歧义的遍历顺序

一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口;调用这个接口,就会返回一个遍历器对象。这样的数据结构才能被称为可迭代对象.

每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值

实现Iterable接口(可迭代协议)要求同时具备两种能力

  • 支持迭代的自我识别能力
  • 创建实现Iterator接口的对象的能力

这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的Symbol.iterator作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器.。

1
2
3
4
5
6
7
8
9
10
interface IteratorResult {
done: boolean;
value: any;
}
interface Iterator {
next(): IteratorResult;
}
interface Iterable {
[Symbol.iterator](): Iterator
}

实际写代码过程中,不需要显式调用工厂函数xxx[Symbol.iterator])来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性,原生语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器。如果对象原型链上的父类实现了Iterable接口,那这个对象也就实现了这个接口。

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next()方法在可迭代对象中遍历数据

  • 成功调用next(),都会返回一个IteratorResult对象
    • IteratorResult对象包含两个对象:done和value
      • done是一个布尔值,表示是否还可以再次调用next()取得下一个值
      • value包含可迭代对象的下一个值(donefalse),或者undefineddonetrue
  • 若不调用next(),则无法知道迭代器的当前位置

迭代器并不知道怎么从可迭代对象中取得下一个值,也不知道可迭代对象有多大。只要迭代器到达done:true状态,后续调用next()就一直返回同样的值了

每个迭代器都表示对可迭代对象的一次性有序遍历。不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象

迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程。如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化

迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象

自定义迭代器

与Iterable接口类似,任何实现Iterator接口的对象都可以作为迭代器使用。

Symbol.iterator属性引用的工厂函数会返回相同的迭代器

提前终止迭代器

可选的return()方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。

可能情形:

  1. for-of循环通过breakcontinuereturnthrow提前退出
  2. 解构操作并未消费所有值

如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。

因为return()方法是可选的,所以并非所有迭代器都是可关闭的。要知道某个迭代器是否可关闭,可以测试这个迭代器实例的return属性是不是函数对象。仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的。这是因为调用return()不会强制迭代器进入关闭状态。即便如此,return()方法还是会被调用

生成器

拥有在一个函数块内暂停和恢复代码执行的能力。使用生成器可以自定义迭代器和实现协程

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。

调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)的状态,生成器对象也实现了Iterator接口,因此具有next()方法。调用这个方法会让生成器开始或恢复执行

next()方法的返回值类似于迭代器,有一个done属性和一个value属性。函数体为空的生成器函数中间不会停留,调用一次next()就会让生成器到达done:true状态

value属性是生成器函数的返回值,默认值为undefined,可以通过生成器函数的返回值指定

生成器函数

  • 只会在初次调用next()方法后开始执行
    • 初次调用生成器函数并不会打印日志
  • 实现了Iterable接口,它们默认的迭代器是自引用的
    • g===g[Symbol.iterator]()

通过yield中断执行

yield关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到yield关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行。

yield关键字

  • 有点像函数的中间返回语句
    • 通过yield关键字退出的生成器函数会处在done:false状态
    • 通过return关键字退出的生成器函数会处于done:true状态
  • 只能在生成器函数内部使用
  • 必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误
  • 可以作为函数的中间参数使用
    • 上一次让生成器函数暂停的yield关键字会接收到传给next()方法的第一个值
    • 第一次调用next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数
  • yield关键字可以同时用于输入和输出
  • 使用星号增强yield的行为,让它能够迭代一个可迭代对象,从而一次产出一个值
    • yield*最有用的地方是实现递归操作,此时生成器可以产生自身

生成器函数内部的执行流程会针对每个生成器对象区分作用域。在一个生成器对象上调用next()不会影响其他生成器

生成器对象作为可迭代对象

提前终止生成器

return()throw()方法都可以用于强制生成器进入关闭状态

  1. 所有生成器对象都有return()方法,只要通过它进入关闭状态,就无法恢复了。后续调用next()会显示done:true状态,而提供的任何返回值都不会被存储或传播
  2. throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭
    • 假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行
    • 错误处理会跳过对应的yield
    • 如果生成器对象还没有开始执行,那么调用throw()抛出的错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误