迭代器和生成器

713 阅读8分钟

阅读《JavaScript高级程序设计(第4版)》第七章及视频讲解

迭代器

迭代的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。

可迭代对象(iterable): 实现了Iterator接口,必须属性”默迭代器(Symbol.iterator)“。

迭代器(iterator):按需创建的一次性对象,next方法返回IteratorResult对象。

IteratorResult对象:done表示是否还有更多值可以访问,true表示“耗尽”,value表示当前值,默认undefined

interface IteratorResult{
	done: boolean;
	value: any
}
interface Iterator{
	next(): IteratorResult
}
interface Iterable{
	[Symbol.iterator](): Iterator
}

可迭代协议

内置类型实现了Iterable接口

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments对象
  • NodeList等DOM集合

接受可迭代对象的原生特性:

  • for-of

  • 数组解构

  • 扩展操作符

  • Array.from()

  • 创建集合

  • 创建映射

  • Promise.all() 接收有Promise组成的可迭代对象

  • Promise.racel() 接收有Promise组成的可迭代对象

  • yield*操作符。生成器中使用

迭代器协议

迭代器next()方法:在可迭代对象中遍历数据,不调用next(),则无法知道迭代器的当前位置。

next()方法返回迭代器对象IteratorResult

const arr = ['foo', 'bar']
let iter = arr[Symbol.iterator]()
console.log(iter.next()) // {done: false, value: 'foo'}
console.log(iter.next()) // {done: false, value: 'bar'}
console.log(iter.next()) // {done: true, value: undefined}

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

console.log(iter.next()) // {done: true, value: undefined}
console.log(iter.next()) // {done: true, value: undefined}

不同迭代器的实例相互之间没有联系。

let iter1 = arr[Symbol.iterator]()
let iter2 = arr[Symbol.iterator]()
console.log(iter1.next()) // {done: false, value: 'foo'}
console.log(iter2.next()) // {done: false, value: 'foo'}

如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化

const arr = ['foo', 'bar']
let iter = arr[Symbol.iterator]()
console.log(iter.next()) // {done: false, value: 'foo'}
arr.splice(1, 0, 'Jane')
console.log(iter.next()) // {done: false, value: 'Jane'}
console.log(iter.next()) // {done: false, value: 'bar'}
console.log(iter.next()) // {done: true, value: undefined}

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

提前终止迭代器

可选的return()方法用于指定在迭代器提前关闭时的执行逻辑。

  • for-of 循环通过break,continue,return 和 throw 提前退出。
  • 解构操作并未消费所有值

return()方法必须返回一个有效的IteratorResult对象,简单情况下,可以只返回{ done: true }

如果迭代器没有关闭,则还可以继续从上一次离开的的地方继续迭代,比如数组的迭代器就是不能关闭的。

const arr = [1, 2, 3, 4]
let iter = arr[Symbol.iterator]()
for(let i of iter){
	console.log(i)
	if(i > 1) break
}
// 1, 2
for(let i of iter){
	console.log(i)
}
// 3, 4

return()方法是可选的。要知道某个迭代器是否可关闭,可以测试这个迭代器实例的return属性是不是函数对象。

给一个不可关闭的迭代器添加return()方法并不能让它变得可关闭。return()方法不会强制迭代器进入关闭状态,但是return()方法还是会调用的。

生成器

生成器是ES6新增加的一个极为灵活的结构,拥有在一个函数内部暂停和恢复代码执行的能力。

生成器基础

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

function* generatorFn(){}
let generatorFn = function* (){}
let foo = {
	* generatorFn(){}
}
class Foo {
 * generatorFn(){}
}
class Foo {
	static * generatorFn(){}
}

注意:箭头函数不能用来定义生成器。

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

next()方法的返回值类似迭代器,有一个done属性和一个value属性。函数体为空的生成器函数中间不会停留。

function* generatorFn(){}
const g = generatorFn()
console.log(g.next()) // {done: true, value: undefined} 函数体为空

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

function* generatorFn(){
	return 'foo'
}
const g = generatorFn()
console.log(g.next()) // {done: true, value: 'foo'}

生成器函数只会在初次调用next()方法后开始执行。

function* generatorFn(){
	console.log('bar')
}
const g = generatorFn() // 初次调用生成器函数并不会打印
g.next() //  bar

生成器对象实现了Iterable接口。它们默认的迭代器是自引用。

通过yield中断执行

yield关键字可以使生成器停止和开始执行。也是生成器最有用的地方。

生成器函数在遇到yield之前会正常执行,之后执行会停止,函数作用域的状态会被保留。

停止执行的生成器只能使用next()方法恢复。

function* generatorFn(){
	yield 'foo';
	yield 'bar';
	return 'Jane'
}
const g = generatorFn() 
console.log(g.next()) // {done: false, value: 'foo'}
console.log(g.next()) // {done: false, value: 'bar'}
console.log(g.next()) // {done: true, value: 'Jane'}

生成器函数会区分生成器对象的作用域,生成器对象之间不会相互影响

...
const g1 = generatorFn() 
const g2 = generatorFn() 
console.log(g1.next()) // {done: false, value: 'foo'}
console.log(g2.next()) // {done: false, value: 'foo'}

yield 关键字只能在生成器函数内部使用。用在其他地方会抛出错误。

  • 嵌套函数
  • 箭头函数
function* generatorFn(){ 
	function a(){
		yield // 错误
	}
}  
function* generatorFn(){ 
	const a = () => {
		yield // 错误
	}
}  
生成器对象作为可迭代对象

在需要自定义迭代对象时,这样使用生成器对象会特别有用。比如,我们需要定义一个可迭代对象,而它会产生一个迭代器,这个迭代器会执行指定次数。

function* nTimes(n){
	while (n--){
		yield;
	}
}
for(let _ of nTimes(3)){
	console.log('foo')
}
// foo  foo  foo
使用yield实现输入输出

yield关键字可以作为函数的中间值参数使用。上一次让生成器函数暂停的yield关键字会接收到传给next()方法的第一个值。第一次调用next()传入的值不会被使用。因为这一次调用时为了开始执行生成器函数。

function* generatorFn(ini){
	console.log(1, ini)
	console.log(2, yield)
	console.log(3, yield)
}
const g = generatorFn('foo') 
g.next('Jane') // 1, foo
g.next('baz')  // 2, baz
g.next('Sue')  // 3, Sue

yield关键字可以同时用于输入和输出

function* generatorFn(){
	return yield 'foo'
}
const g = generatorFn() 
console.log(g.next())     // {value: "foo", done: false}
console.log(g.next('bar'))  // {value: "bar", done: true}
// 因为函数必须对整个表达式求值才能确定要返回的值,所以它遇到yield关键字时暂停执行并计算出要产生的值:’foo‘
// 下一次调用next()传入了'bar',作为交给同一个yield的值。然后这个值被确定为本次生成器函数要返回的值。
产生可迭代对象

可以使用星号增强yield的行为,让它能够迭代一个可迭代对象,从而一次产出一个值。

function* generatorFn(){
	yield* [1, 2, 3]
}
const g = generatorFn() 
for(const x of g){
	console.log(x)
}
// 1  
// 2  
// 3
// 因为yield*实际上知识将一个可迭代对象序列化为一连串可以单独产出的值,所以这根把yield放到一个循环里没有什么不同。
使用yield*实现递归算法

yield*最有用的地方是实现递归操作,此时生成器可以产生自身

function* nTimes(n){
	if(n > 0){
    yield* nTimes(n-1)
    yield n-1
	}
}
for(const x of nTimes(3)){
	console.log(x)
}
// 1  
// 2  
// 3
// 每个生成器首先都会从新创建的生成器对象产出每个值,然后再产出一个整数。结果就是生成器函数会递归地减少计数器值,并实例化另一个生成器对象。

生成器作为默认迭代器

因为生成器对象实现了Iterator接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器格外适合作为默认迭代器。

class Foo {
	constructor(){
		this.value = [1, 2, 3]
	}
	* [Symbol.iterotor](){
		yield* this.value
	}
}
const f = new Foo()
for (const x of f){
	console.log(x)
}
// 1  
// 2  
// 3
// for-of 循环调用了默认迭代器并产生了一个生成器对象。这个生成器对象是可迭代的。

提前终止生成器

与迭代器类似,生成器也支持’可关闭‘'的概念。一个实现了Iterator接口的对象一定有next()方法,还有一个可选的return()方法用于提前终止迭代器。生成器对象除了有这两个方法,还要第三个方法:throw()。

return()

return()方法会强制生成器进入关闭状态。提供return()方法的值,就是终止迭代器对象的值。

与迭代器不同,所有生成器对象都有return()方法,只要通过它进入关闭状态,就无法恢复了。后续调用next(),会显示done:true,而且提供任何返回值都不会被存储或传播。

function* generatorFn(){
	for(const x of [1, 2, 3]){
		yield x
	}
}
const g = generatorFn() 
console.log(g.next())   		// {value: 1, done: false}
console.log(g.return(4)) 		// {value: 4, done: true}
console.log(g.next('rrr'))	// {value: undefined, done: true}
console.log(g.next())				// {value: undefined, done: true}

for-of等内置语言结构会忽略状态为done:true的IteratorObject内部返回值。

function* generatorFn(){
	for(const x of [1, 2, 3]){
		yield x
	}
}
const g = generatorFn() 
for(const x of g ){
	if(x > 1){
		g.return(5)
	}
	console.log(x)
}
// 1
// 2
console.log(g.next())				// {value: undefined, done: true}
throw()

throw()会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未处理,生成器就会关闭。

function* generatorFn(){
	for(const x of [1, 2, 3]){
		yield x
	}
}
const g = generatorFn()
console.log(g.next())		// {value: 1, done: false}
try{
	g.throw('foo')
}catch(e){
	console.log(e)  // foo
}
console.log(g.next()) 	// {value: undefined, done: true}

假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行,错误处理会跳过对应的yield。

function* generatorFn(){
	for(const x of [1, 2, 3]){
		try{
			yield x
		} catch(e){}
	}
}
const g = generatorFn()
console.log(g.next())		// {value: 1, done: false}
g.throw('foo')
console.log(g.next()) 	// {value: 3, done: false}
// 错误是在生成器的try/catch块中抛出的,所以在生成器内部捕获,可是,由于yield抛出了那个错误,生成器就不会再产出值2了,下一次迭代在遇到yield关键字时产出了值3.

如果生成器对象还没有开始执行,那么调用throw()抛出的错误不会再函数内部捕获,因为这相当于在函数块外抛出错误。