迭代器与生成器

小结

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

生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了 Iterable 接口,因此可用在任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,这个关键字能够暂停执行生成器函数。使用 yield 关键字还可以通过 next() 方法接收输入和产出输出。在加上 * 之后,yield 可以将跟在它后面的可迭代对象序列化为一连串值

理解迭代

在 ECMAScript 较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码里增加,代码会变得越发混乱。很多语言都通过原生语言结构解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式

迭代器模式

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

可迭代协议

很多内置对象都实现了 Iterable 接口(可迭代协议):

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

检查是否存在默认迭代器属性可以暴露这个工厂函数(调用工厂函数会生成一个迭代器):

function isIterable(object) {
	return typeof object[Symbol.iterator] === 'function'
}

实际写代码过程中,不需要显示调用这个工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性

接收可迭代对象的语言特性包括:

  • for-of 循环
  • 解构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all() 接收由期约组成的可迭代对象
  • Promise.race() 接收由期约组成的可迭代对象
  • yield* 操作符,在生成器中使用

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next() 方法在可迭代对象中遍历数据。每次调用 next() 方法都会返回一 IteratorResult 对象,这个结果对象包含两个属性:done 和 value

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

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

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

注意

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

自定义迭代器

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

class Counter {
	constructor(limit) {
		this.limit = limit
	}

	[Symbol.iterator]() {
		let count = 1
		const limit = this.limit
		return {
			next() {
				if (count <= limit) {
					return { done: false, value: count++ }
				} else {
					return { done: true, value: undefined }
				}
			},
		}
	}
}
const counter = new Counter(3)

for (const i of counter) {
	console.log(i)
}

每个以这种方式创建的迭代器也实现了 Iterable 接口,并且 Symbol.iterator 属性引用的工厂函数会返回相同的迭代器

const arr = ['foo', 'bar', 'baz']
const iter1 = arr[Symbol.iterator]()
console.log(iter1[Symbol.iterator]) // ƒ [Symbol.iterator]() { [native code] }
const iter2 = iter1[Symbol.iterator]()
console.log(iter1 === iter2)



 

提前终止迭代器

可选的(意味着并不是所有的迭代器都是可关闭的,可以测试这个迭代器实例的 return 属性是不是函数来判断该迭代器是否可关闭) return() 方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。可能的情况包括:

  • for-of 循环通过 break / continue / return / throw 提前退出
  • 解构操作并未消费所有值
class Counter {
	constructor(limit) {
		this.limit = limit
	}

	[Symbol.iterator]() {
		let count = 1
		const limit = this.limit
		return {
			next() {
				if (count <= limit) {
					return { done: false, value: count++ }
				} else {
					return { done: true, value: undefined }
				}
			},
			return() {
				console.log('Exiting early')
				return { done: true }
			},
		}
	}
}
const counter = new Counter(5)

for (const i of counter) {
	if (i > 2) {
		break
	}

	console.log(i)
}

const [a, b] = counter
















 
 
 
 














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

const a = [1, 2, 3, 4, 5]
const iter = a[Symbol.iterator]()

for (const i of iter) {
	console.log(i)
	if (i > 2) {
		break
	}
}
// 1
// 2
// 3

for (const i of iter) {
	console.log(i)
}
// 4
// 5

注意,仅仅给一个不可关闭的迭代器增加 return() 方法并不能让它变成可关闭的,这个因为调用 return() 并不会强制迭代器进入关闭状态。即便如此,return() 方法还是会被调用:

const a = [1, 2, 3, 4, 5]
const iter = a[Symbol.iterator]()

iter.return = function () {
	console.log('Exiting early')
	return { done: true }
}

for (const i of iter) {
	console.log(i)
	if (i > 2) {
		break
	}
}
// 1
// 2
// 3
// Exiting early

for (const i of iter) {
	console.log(i)
}
// 4
// 5



 
 
 
 

















生成器

生成器的形式是一个函数,函数名称前面加一个 * 表示它是一个生成器,标识生成器的星号不受两侧空格的影响。只要是可以定义函数的地方就可以定义生成器

注意

箭头函数不能用来定义生成器函数

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

function* generatorFn() {}

const g = generatorFn()
console.log(g) // generatorFn {<suspended>}
console.log(g.next()) // {value: undefined, done: true}

next() 方法的返回值类似于迭代器,由一个 done 属性和一个 value 属性

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

function* generatorFn() {
	return 'foo'
}

const generatorObject = generatorFn()
console.log(generatorObject) // generatorFn {<suspended>}
console.log(generatorObject.next()) // {value: 'foo', done: true}

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

通过 yield 中断执行

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

yield 关键字生成的值会出现在 next() 方法返回的对象里。通过 yield 退出的生成器函数会处在 done:false 状态;通过 return 退出的生成器函数会处于 done:true 状态

生成器函数内部的执行流程会针对每个生成器对象区分作用域

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

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

在生成器对象上显示调用 next() 方法的用处并不大。如果把生成器当作可迭代对象:

function* generatorFn() {
	yield 1
	yield 2
	yield 3
}

for (const i of generatorFn()) {
	console.log(i)
}
// 1
// 2
// 3

2. 使用 yield 实现输入和输出

除了作为函数的中间返回语句使用,yield 还可以作为函数的中间参数使用。yield 语句的值可以通过 next() 方法的参数传入生成器函数

function* generatorFn(initial) {
	console.log(initial)
	console.log(yield)
	console.log(yield)
}

const generatorObject = generatorFn('foo')
generatorObject.next('bar') // foo
generatorObject.next('baz') // baz
generatorObject.next('qux') // qux

yield 可以同时用于输入和输出

function* generatorFn() {
	return yield 'foo'
}

const generatorObject = generatorFn()
console.log(generatorObject.next()) // {value: 'foo', done: false}
console.log(generatorObject.next('bar')) // {value: 'bar', done: true}

使用生成器填充数组

function* zeros(n) {
	for (let i = 0; i < n; i++) {
		yield 0
	}
}
console.log(Array.from(zeros(3)))

3. 产生可迭代对象 可以使用星号(两侧的空格不影响)增强 yield 的行为,让它那能够迭代一个可迭代对象,从而一次产出一个值:

function* generatorFn() {
	yield* [1, 2, 3]
}

for (const i of generatorFn()) {
	console.log(i)
}
// 1
// 2
// 3

yield* 的值是关联迭代器返回 done: true 时的 value 属性

4. 使用 yield* 实现递归算法

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

function* nTimes(n) {
	if (n > 0) {
		yield* nTimes(n - 1)
		yield n - 1
	}
}

for (const i of nTimes(3)) {
	console.log(i)
}
// 0
// 1
// 2

生成器作为默认迭代器

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

class Foo {
	constructor() {
		this.values = [1, 2, 3]
	}

	*[Symbol.iterator]() {
		yield* this.values
	}
}

提前终止生成器

1. return()

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

所有的生成器都有 return(),只要通过它进入关闭状态,就无法恢复了

function* generatorFn() {
	yield* [1, 2, 3]
}

const g = generatorFn()
console.log(g) // generatorFn {<suspended>}
console.log(g.return(4)) // {value: 4, done: true}
console.log(g) // generatorFn {<closed>}

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

function* generatorFn() {
	yield* [1, 2, 3]
}

const g = generatorFn()

for (const i of g) {
	if (i > 1) {
		g.return(4)
	}

	console.log(i)
}
// 1
// 2

2. throw()

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

function* generatorFn() {
	yield* [1, 2, 3]
}

const g = generatorFn()

console.log(g) // generatorFn {<suspended>}
try {
	g.throw(new Error('foo'))
} catch (e) {
	console.log(e) // Error: foo
}
console.log(g) // generatorFn {<closed>}

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

function* generatorFn() {
	for (const x of [1, 2, 3]) {
		try {
			yield x
		} catch (e) {
			console.log(e)
		}
	}
}

const g = generatorFn()

console.log(g.next()) // {value: 1, done: false}
g.throw('foo')
console.log(g.next()) // {value: 3, done: false}
Last Updated: