第7章 迭代器与生成器
# 迭代器与生成器
迭代是按照顺序反复多次执行一段程序,通常有明确的终止条件。
循环是迭代的基础,使用索引来进行遍历并不能满足所有数据结构的迭代,比如 Map,Set,因为他们可能没有索引。
Array.prototype.forEach
不需要索引了,但不能终止迭代,而且仅限于数组使用。
# 迭代器模式
迭代器模式规定了一些数据结构,成为“可迭代对象”,它们实现了正式的 Iterable 接口,并且可以通过迭代器 Iterator 消费。
“可迭代对象”,比如数组,Map,Set 等有序集合
# 可迭代协议
默认迭代器属性:Symbol.iterator
,它指向一个迭代器工厂函数,返回一个迭代器。
已实现了 Iterator 接口的数据类型:
- 字符串
- 数组
- 映射
- 集合
- arguments 对象
- NodeList 等 DOM 集合类型
let str = 'abc'
let arr = ['a', 'b', 'c']
let map = new Map().set('a', 1).set('b', 2)
let set = new Set().add('a').add('b')
console.log(str[Symbol.iterator]()) // Object [String Iterator] {}
console.log(arr[Symbol.iterator]()) // Object [Array Iterator] {}
console.log(map[Symbol.iterator]()) // [Map Entries] { [ 'a', 1 ], [ 'b', 2 ] }
console.log(set[Symbol.iterator]()) // [Set Iterator] { 'a', 'b' }
let noop = function() {
console.log(arguments[Symbol.iterator]()) // Object [Array Iterator] {}
}
noop('a', 'b', 'c')
2
3
4
5
6
7
8
9
10
11
12
13
js 有原生语言特性来隐式调用这个工厂函数,从而生成迭代器,包括:
- for-of 循环
- 数组解构
- 扩展操作符
- Array.from()
- 创建集合(Set)和映射(Map)
- Promise.all()和 Promise.race()接收可迭代对象
- yield*操作符
let arr = [1, 2, 3]
// for-of
for (let item of arr) {
console.log(item) // 1 2 3
}
// 数组解构
let [a, b, c] = arr
console.log(a, b, c) // 1 2 3
// 扩展操作符
let arr2 = [...arr]
console.log(arr2) // [ 1, 2, 3 ]
// Array.from()
let arr3 = Array.from(arr)
console.log(arr3) // [ 1, 2, 3 ]
// Set
let set = new Set(arr)
console.log(set) // Set(3) { 1, 2, 3 }
// Map
let pairs = arr.map((x) => ['prefix' + x, x])
let map = new Map(pairs)
console.log(map) // Map(3) { 'prefix1' => 1, 'prefix2' => 2, 'prefix3' => 3 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 迭代器协议
迭代器通过迭代器协议,来迭代与其关联的可迭代对象。
迭代器调用 next()方法来遍历数据,每次调用 next()都会返回 IteratorResult 对象,包含 2 个属性:done 和 value。
done 为 true 时,表示遍历完成,此时 value 为 undefined;为 false 时,value 为可迭代对象中的当前遍历到的值。
let arr = ['foo', 'bar']
const iter = arr[Symbol.iterator]()
console.log(iter.next()) // { value: 'foo', done: false }
console.log(iter.next()) // { value: 'bar', done: false }
console.log(iter.next()) // { value: undefined, done: true }
console.log(iter.next()) // { value: undefined, done: true }
2
3
4
5
6
7
当 done 为 true 后,之后再调用就一直返回同样的值。
不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象。
# 自定义迭代器
class Counter {
constructor(limit) {
this.count = 1
this.limit = limit
}
next() {
if (this.count <= this.limit) {
return { done: false, value: this.count++ }
} else {
return { done: true, value: undefined }
}
}
[Symbol.iterator]() {
return this
}
}
const counter = new Counter(3)
for (let item of counter) {
console.log(item) // 1 2 3
}
for (let item of counter) {
console.log(item) // (nothing logged)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
上述代码实现了 next()和Symbol.iterator方法,即实现了 Iterator 接口。但每个实例只能迭代一次。下面利用闭包来存储计数器变量,从而可以创建多个迭代器。
class Counter {
constructor(limit) {
this.limit = limit
}
[Symbol.iterator]() {
let count = 1,
limit = this.limit
return {
next() {
if (count <= limit) {
return { done: false, value: count++ }
} else {
return { done: true, value: undefined }
}
},
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 提前终止迭代器
可选的return()
方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。
这句话有点拗口。意思是迭代器暴露一个和 next 同级的 return 方法,调用方如 for...of 结构可以在遍历未完成时,退出遍历。
可能的情况有 2 类:
- for...of 的 break、continue、return、throw 语句提前退出
- 解构一部分值
class Coutner {
constructor(limit) {
this.limit = limit
}
[Symbol.iterator]() {
let limit = this.limit,
count = 1
return {
next() {
if (count <= limit) {
return { done: false, value: count++ }
} else {
return { done: true, value: undefined }
}
},
return() {
return { done: true }
},
}
}
}
const counter = new Coutner(3)
for (let item of counter) {
console.log(item) // 1 2 3
}
for (let item of counter) {
if (item < 3) {
console.log(item) // 1 2
} else {
break
}
}
const [a, b] = counter
console.log(a, b) // 1 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 生成器
生成器拥有在一个函数块内暂停和恢复代码执行的能力。
# 生成器基础
只要是可以定义函数的地方,就可以定义生成器,通常写法为
function* generatorFn() {}
调用生成器函数会产生一个生成器对象,生成器对象实现了 Iterator 接口,因此有 next()方法,返回一个对象,包含 done 和 value 属性。
function* generatorFn() {
console.log('start')
return 'foo'
}
// 调用生成器函数不会执行里面的代码
const g = generatorFn()
console.log(g === g[Symbol.iterator]()) // true
// 在初次调用next方法后,生成器里面的代码才执行
console.log(g.next()) // start { value: 'foo', done: true }
2
3
4
5
6
7
8
9
10
11
# 通过 yield 中断执行
yield 关键字可以让生成器停止和开始执行。具体是遇到 yield 后,执行停止,只能通过生成器对象调用 next 方法恢复执行。yield 有点像函数的中间返回语句,它生成的值会出现在 next()方法返回的对象里。
function* generatorFn() {
yield 'foo'
yield 'bar'
return 'baz'
}
const g = generatorFn()
console.log(g.next()) // { value: 'foo', done: false }
console.log(g.next()) // { value: 'bar', done: false }
console.log(g.next()) // { value: 'baz', done: true }
console.log(g.next()) // { value: undefined, done: true }
2
3
4
5
6
7
8
9
10
yield 关键字只能直接在生成器函数内部使用,即必须配合*使用。
生成器对象作为可迭代对象使用
function* generatorFn() { yield 1 yield 2 yield 3 } for (let x of generatorFn()) { console.log(x) // 1 2 3 }
1
2
3
4
5
6
7
8使用 yield 实现输入输出
yield 关键字还可以作为函数的中间参数使用,上一次让生成器函数暂停的 yield 关键字会接收到传给 next()方法的第一个值。
function* generatorFn(initial) { console.log('1', initial) console.log('2', yield) console.log('3', yield) } const g = generatorFn('foo') g.next('bar') // 1 foo g.next('baz') // 2 baz g.next('qux') // 3 qux
1
2
3
4
5
6
7
8
9
10因此 yield 可以同时用于输入和输出。
function* generatorFn() { return yield 'foo' // 上一行等同于 // const a = yield 'foo' // console.log('a', a) // bar // return a } const g = generatorFn() console.log(g.next()) // { value: 'foo', done: false } console.log(g.next('bar')) // { value: 'bar', done: true }
1
2
3
4
5
6
7
8
9
10产生可迭代对象
可以使用星号增强 yield 的行为,让它能够迭代一个可迭代对象,从而一次产生一个值。
// function* generatorFn() { // for (const x of [1, 2, 3]) { // yield x // } // } // 等同于 function* generatorFn() { yield* [1, 2, 3] } for (let x of generatorFn()) { console.log(x) // 1 2 3 }
1
2
3
4
5
6
7
8
9
10
11
12再来看一个例子
function* generatorFn() { yield 'foo' yield 'baz' return 'bar' } function* outerGenerationFn() { console.log('iter value', yield* generatorFn()) // bar } for (let x of outerGenerationFn()) { console.log(x) // foo baz }
1
2
3
4
5
6
7
8
9
10
11
12说明 yield*的值是关联迭代器返回 done:true 时的 value 属性。
使用 yield*产生递归算法
举个简单例子
function* nTimes(n) { if (n > 0) { yield* nTimes(n - 1) yield n - 1 } } for (const x of nTimes(3)) { console.log(x) // 0 1 2 }
1
2
3
4
5
6
7
8
9
10
# 生成器作为默认迭代器
因为生成器对象实现了 Iterable 接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器格外适合作为默认迭代器。
class Foo {
constructor() {
this.values = [1, 2, 3]
}
*[Symbol.iterator]() {
yield* this.values
}
}
const f = new Foo()
for (const x of f) {
console.log(x)
}
2
3
4
5
6
7
8
9
10
11
12
# 提前终止生成器
生成器也支持“可关闭”,生成器对象除了有 next()和 return()方法,还有 throw(),都可以提前终止迭代器。