迭代器模式

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

可迭代对象不一定是集合对象,也可以是具有类似数组行为的其他数据结构,如下的计算循环:

1
2
3
for(let i=0;i<10;i++){
console.log(i)
}

可迭代协议

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

  • 支持迭代的自我识别能力
  • 创建实现 Iterator 接口的对象的能力
    在ECMAScript中,必须暴露一个属性作为 “默认迭代器”,而且这个属性必须使用特殊的 Symbol.iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器

很多内置类都实现了 Iterable 接口

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

检查是否存在默认迭代器属性可以暴露这个工厂函数:

1
2
3
4
5
6
7
8
9
10
11
let num = 1;
let obj = {}
// 数字和对象没有实现迭代器工厂函数
num[Symbol.iterator] // undefined
obj[Symbol.iterator] // undefined

let str = 'abc'
str[Symbol.iterator] // f values() { [native code] }

// 调用这个工厂函数会生成一个迭代器
str[Symbol.iterator]() // StringIterator {}

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

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

如果对象原型链上的父类实现了 Iterable 接口,那这个对象也就实现了这个接口:

1
2
3
4
5
6
7
8
class FooArray extend Array {}
let fooArr = new FooArray('foo','bar','baz')
for(let value of fooArr){
console.log(value)
}
// 'foo'
// 'bar'
// 'baz'

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next() 方法在可迭代对象中遍历数据。每次成功调用 next(),都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值。若不调用 next(),则无法知道迭代器的当前位置
next() 方法返回的迭代器对象 IteratorResult 包含两个属性:done 和 value。done 是一个布尔值,表示迭代是否完成;value 包含可迭代对象的下一个值(done 为 false 时)、或者 undefined(done 为 true 时)。done:true 状态成为 “耗尽”(个人理解为迭代完成

1
2
3
4
5
6
7
8
let ary = ['foo','bar']
let iter = ary[Symbol.iterator]()
console.log(iter) // ArrayIterator {}

// 执行迭代
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() 就一直返回同样的值了

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

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

1
2
3
4
5
6
7
8
let arr = ['foo','bar']
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' }
console.log(iter2.next()) // { done: false, value: 'bar' }
console.log(iter2.next()) // { done: false, value: 'bar' }

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

1
2
3
4
5
6
7
8
9
10
let arr = ['foo','bar']
let iter = arr[Symbol.iterator]()

console.log(iter.next()) // { done: false, value: 'foo' }

arr.splice(1,0,'baz')

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

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

自定义迭代器

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

1
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
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 }
}
}
}
}
}
let counter = new Counter(3)
for(let i of coutner) {
console.log(i)
}
/*
* 1
* 2
* 3
*/

生成器

生成器是 ES6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。比如:可以使用生成器自定义迭代器和实现协程

生成器基础

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

注意
箭头函数不能用来定义生成器函数
标识生成器函数的星号不受两侧空格的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 生成器函数声明
function *generatorFn() {}

// 生成器函数表达式
let generatorFn = function * {}

// 作为对象字面量方法的生成器函数
let foo = {
* generatorFn() {}
}

// 作为类实例方法的生成器函数
class Foo(){
*generatorFn() {}
}

// 作为类静态方法的生成器函数
class Bar {
static* generatorFn() {}
}

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

1
2
3
4
function * generator(){}
const g = generator()
console.log(g); // generator {<suspended>}
console.log(g.next) // f next() { [native code] }

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

1
2
// 代码接上
console.log(g.next()) { done: true, value: undefined }

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

1
2
3
4
5
6
7
function* generatorFn() {
return 'foo'
}
let generatorObject = generatorFn()

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

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

1
2
3
4
5
6
7
function* generatorFn() {}
conosle.log(generatorFn) // f* generatorFn() {}
console.log(generatorFn()[Symbol.iterator]) // f [Symbol.iterator]() {native code}

const g = generatorFn()
console.log(g) // generator {<suspended>}
console.log(g === g[Symbol.iterator]()) // true

通过 yield 中断执行

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

1
2
3
4
5
6
7
8
function* generatorFn() {
yield;
}

let generatorObject = generatorFn()

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

yield 关键字退出的生成器函数会处在 done: false 状态,value 为 yield 的值,通过 return 关键字退出的生成器状态为 done: true,值为 return 的值

1
2
3
4
5
6
7
8
9
10
function* generator(){
yield 'foo';
yield 'bar';
return 'baz'
}
const g = generator()
console.log(g.next()) // { done: false, value: 'foo' }
console.log(g.next()) // { done: false, value: 'bar' }
console.log(g.next()) // { done: true, value: 'baz' }
console.log(g.next()) // { done: true, value: undefined }

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

yield 关键字只能在生成器函数内部使用,用在其他地方会抛出错误;且必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 有效
function* validGenerator() {
yield;
}

// 无效
function* invalidGenerator() {
function a() {
yield;
}
}

// 无效
function* invalidGeneratorA() {
const b = () => {
yield;
}
}

// 无效
function* invalidGeneratorB() {
(()=>{
yield;
})()
}