迭代器与生成器

迭代器与生成器

JavaScript 当中对集合中每个元素进行处理是很常见的操作,比如数组遍历、对象的属性遍历,以往这些操作是通过 for 循环、.forEach.map 等方式进行,借由迭代器机制为 MapArraystring 等对象提供了统一的遍历语法,以及更方便的相互转换

在最新的 ES6 当中为方便编写迭代器还提供了生成器(Generator)语法,迭代器和生成器将迭代的概念直接带入核心语言,并提供了一种机制来自定义 for-of 循环的行为,下面我们就来看看迭代器到底是一种怎样的语法

迭代器

所谓迭代器,其实简单来说就是一个具有 next() 方法的对象,每次调用 next() 方法都必须要返回一个『对象』,被返回对象拥有两个属性(如果返回一个非对象值,则需要抛出一个错误)

  • done,布尔值,表示遍历是否结束,如果为 true,则表示迭代器已经超过了可迭代次数,在这种情况下 value 的值可以被省略,如果迭代器可以产生序列中的下一个值,则为 false
  • value,表示当前的值,迭代器可以返回的任何 JavaScript 值,在 donetrue 的时候可省略

比如我们来看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function createIterator(items) {
var i = 0
return {
next: function () {
var done = i >= items.length
var value = !done ? items[i++] : undefined
return {
done: done,
value: value
}
}
}
}

// iterator 就是一个迭代器对象
var iterator = createIterator([1, 2, 3])

iterator.next() // { done: false, value: 1 }
iterator.next() // { done: false, value: 2 }
iterator.next() // { done: false, value: 3 }
iterator.next() // { done: true, value: undefined }

在了解了上面的例子以后,我们就会考虑,那么这样一来我们的迭代器对象是不是就可以进行遍历了呢?,我们来试一下

1
2
3
4
5
6
7
var iterator = createIterator([1, 2, 3])

for (let value of iterator) {
console.log(value)
}

// iterator is not iterable

结果会发现运行报错,那么这就表明我们生成的 iterator 对象并不是 iterable(可遍历的),那么什么又是可遍历的呢?在 ES6 当中规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说一个数据结构只要具有 Symbol.iterator 属性,就可以认为是可遍历的(iterable),简单来说就是,只要『一种数据结构部署了 Iterator 接口,我们就称这种数据结构是可遍历的(iterable)』,比如下面这个例子

1
2
3
4
5
6
7
8
9
10
const obj = {
value: 1
}

for (value of obj) {
console.log(value)
}

// 直接运行会报错
// TypeError: iterator is not iterable

如果我们直接使用 for-of 遍历一个对象会报错,然而如果我们给该对象添加 Symbol.iterator 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {
value: 1
}

obj[Symbol.iterator] = function () {
return createIterator([1, 2, 3])
}

for (value of obj) {
console.log(value)
}

// 1
// 2
// 3

由此可以发现,for-of 遍历的其实是对象的 Symbol.iterator 属性,这里就可以引出我们的迭代器协议

迭代器协议

『迭代器协议』又称生成器协议,该协议定义了什么是迭代器对象,迭代器协议定义了一种标准的方式来产生一个有限或无限序列的值,并且当所有的值都已经被迭代后,就会有一个默认的返回值,一些内置类型都是内置的可迭代类型并且有默认的迭代行为,比如 Array 或者 Map,另一些类型则不是(比如 Object,下方会进行介绍)

当使用 for-of 循环遍历某种数据结构时,它会首先调用被遍历集合对象的 Symbol.iterator() 方法,该方法返回一个迭代器对象,它的基本语法为

1
2
3
4
5
6
var myIterator = {
next: function () {
// ...
},
[Symbol.iterator]: function () { return this }
}

比如 string,就是一个内置的可迭代对象

1
2
3
var str = 'hi'

typeof str[Symbol.iterator] // 'function'

string 的默认迭代器会一个接一个返回该字符串的字符

1
2
3
4
5
6
const str = 'hi'
const iterator = str[Symbol.iterator]()

iterator.next() // {value: 'h', done: false}
iterator.next() // {value: 'i', done: false}
iterator.next() // {value: undefined, done: true}

我们也可以通过自己的 @@iterator 方法重新定义迭代行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var str = new String('hi')

str[Symbol.iterator] = function () {
return {
next: function () {
if (this._first) {
this._first = false
return { value: 'hello', done: false }
} else {
return { done: true }
}
},
_first: true
}
}

for (let i of str) {
// hello
console.log(i)
}

在对象上实现 Iterator 接口

在上面我们提到过,stringArrayTypedArrayMapSet 是所有内置可迭代对象,因为它们的原型对象都有一个 @@iterator 方法,而针对于对象(Object)默认是没有 Iterator 接口的,如果我们想让它变为可遍历的,有两种方法

  • 一种是在其 [Symbol.iterator] 属性当中实现一个上文所述的 next 方法
  • 或者像上方基本语法一样,在外部实现 next() 方法,然后在 [Symbol.iterator] 当中返回 this 也可

那么这里就会存在一个问题,为什么 stringArray 等对象都有部署 Iterator 接口,而偏偏 Object 没有呢?其实是有两个原因

  • 一是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定,然而遍历遍历器是一种线性处理,对于非线性的数据结构,部署遍历器接口,就等于要部署一种线性转换
  • 二是对对象部署 Iterator 接口并不是很必要,因为 Map 弥补了它的缺陷,又正好有 Iteraotr 接口

但是我们可以手动的为对象添加一个 Iterator 接口,比如下面这个实现 50 以内的斐波纳契数列的示例

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
let obj = {
[Symbol.iterator]: function () {
let a = 0, b = 0
return {
next: function () {
let value = 0
if (!a) {
value = a = 1
} else if (!b) {
value = b = 1
} else if (b < 50) {
value = a + b
a = b
b = value
}
return { done: value === 0, value }
}
}
}
}

for (let i of obj) {
// 1 1 2 3 5 8 13 21 34 55
console.log(i)
}

模拟实现 for-of

下面我们可以尝试模拟一下 for-of 的实现,简单来说,就是通过 Symbol.iterator 获取迭代器对象,然后使用 while 遍历,当迭代器的 donefalse 的时候退出循环,因为迭代器对象既然可以被 for-of 遍历,那么它肯定就存在 Symbol.iterator属性

1
2
3
4
5
6
7
8
9
10
11
12
function forOf(obj, cb) {
let iterable, result
if (typeof obj[Symbol.iterator] !== 'function')
throw new TypeError(result + ' is not iterable')
if (typeof cb !== 'function') throw new TypeError('cb must be callable')
iterable = obj[Symbol.iterator]()
result = iterable.next()
while (!result.done) {
cb(result.value)
result = iterable.next()
}
}

内建迭代器

为了更好的访问对象中的内容,比如有的时候我们仅需要数组中的值,但有的时候不仅需要使用值还需要使用索引,在 ES6 当中为数组,MapSet 集合内建了以下三种迭代器

  • entries(),返回一个遍历器对象,用来遍历 [key, value] 组成的数组,对于数组,键名就是索引值
  • keys(),返回一个遍历器对象,用来遍历所有的键名
  • values(),返回一个遍历器对象,用来遍历所有的键值

比如以数组为例

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
var colors = ['red', 'green', 'blue']

for (let index of colors.keys()) {
console.log(index)
}

// 0
// 1
// 2

for (let color of colors.values()) {
console.log(color)
}

// red
// green
// blue

for (let item of colors.entries()) {
console.log(item)
}

// [ 0, 'red' ]
// [ 1, 'green' ]
// [ 2, 'blue' ]

Map 类型与数组类似,但是对于 Set 类型需要注意

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
var colors = new Set(['red', 'green', 'blue'])

for (let index of colors.keys()) {
console.log(index)
}

// red
// green
// blue

for (let color of colors.values()) {
console.log(color)
}

// red
// green
// blue

for (let item of colors.entries()) {
console.log(item)
}

// [ 'red', 'red' ]
// [ 'green', 'green' ]
// [ 'blue', 'blue' ]

通过上面的例子可以发现,Set 类型的 keys()values() 返回的是相同的迭代器,这也意味着在 Set 这种数据结构中键名与键值相同,这里需要注意一点,每个集合类型都是有一个默认的迭代器的,在 for-of 循环中,如果没有显式指定则使用默认的迭代器,则

  • 数组和 Set 集合的默认迭代器是 values() 方法
  • Map 集合的默认迭代器是 entries() 方法

这也就是为什么直接 for-of 遍历 SetMap 数据结构,会有不同的数据结构返回,这里有一个小技巧,就是遍历 Map 数据结构的时候可以结合解构赋值来输出想要的格式

1
2
3
4
5
6
7
8
const valuess = new Map([['key1', 'value1'], ['key2', 'value2']])

for (let [key, value] of valuess) {
console.log(key + ' ==> ' + value)
}

// key1 ==> value1
// key2 ==> value2

生成器对象

生成器对象是由一个 Generator 函数(function*)返回的,并且它符合可迭代协议和迭代器协议

1
2
3
4
5
6
7
8
9
10
function* g() {
yield 1
yield 2
}

var iterator = g()

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: undefined, done: true }

生成器函数在执行时能暂停,后面又能从暂停处继续执行,调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器(iterator)对象,当这个迭代器的 next() 方法被首次调用时,其内的语句会执行到第一个出现 yield 的位置为止,yield 后紧跟迭代器要返回的值,或者如果用的是 yield*(有星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行),而 next() 方法则会返回一个对象,这个对象包含两个属性 valuedone

  • value 属性表示本次 yield 表达式的返回值
  • done 属性为布尔类型,表示生成器后续是否还有 yield 语句,即生成器函数是否已经执行完毕并返回

这里有一个需要注意的地方,如果在生成器函数当中使用了 return ,会立即结束执行,done 会立即变为 true

1
2
3
4
5
6
7
8
9
10
11
function* g() {
yield 1
return 2
yield 2
}

var iterator = g()

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: true }
iterator.next() // { value: undefined, done: true }

在调用 next() 方法的时候,如果传入了参数,那么这个参数会作为上一条执行的 yield 语句的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
function* g() {
let first = yield 1
let second = yield first + 2 // 10 + 2,first 的值是由 next(10) 传递的
yield second + 3 // 5 + 3,需要注意的是,如果在调用的时候不传递参数,则会返回
// { value: NaN, done: false }
}

let iterator = g()

iterator.next() // { value: 1, done: false }
iterator.next(10) // { value: 12, done: false }
iterator.next(5) // { value: 8, done: false }
iterator.next() // { value: undefined, done: true }

现在可以使用生成器方法重新实现之前的的斐波纳契数列示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let obj = {
[Symbol.iterator]: function* () {
let a = 1, b = 1
yield a
yield b
while (b < 50) {
yield b = a + b
a = b - a
}
}
}

for (let i of obj) {
// 1 1 2 3 5 8 13 21 34 55
console.log(i)
}

yield*

yield* 表达式用于委托给另一个 Generator 或可迭代对象,下面是一个简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* g1() {
yield 2
}

function* g2() {
yield 1
yield* g1()
yield 3
}

var iterator = g2()

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: undefined, done: true }

除了生成器对象这一种可迭代对象,yield* 还可以 yield 其它任意的可迭代对象,比如说数组、字符串、arguments 对象等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* g3() {
yield* [1, 2]
yield* '34'
yield* arguments
}

var iterator = g3(5, 6)

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: '3', done: false }
iterator.next() // { value: '4', done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: 6, done: false }
iterator.next() // { value: undefined, done: true }

生成器也可以接收参数

1
2
3
4
5
6
7
8
9
10
function* g() {
var index = arguments[0] || 0
while (true)
yield index++
}

var gen = g(5)

gen.next().value // 5
gen.next().value // 6

Map,Set,String,Array 互相转换

可迭代协议给出了统一的迭代协议,使得不同类型的集合间转换更加方便,以下是一些很方便的转换技巧,比如从 Array 生成 Set,可用于数组去重

1
2
3
4
5
new Set(['1', '2', '3'])

// 等价于(但不是 ===)

new Set(['1', '2', '3'][Symbol.iterator]())

Set 得到 Array

1
2
3
4
5
Array.from(new Set(['1', '2', '3']))  // ['1', '2', '3']

// 等价于

Array.from(['1', '2', '3'][Symbol.iterator]())

除了 for-of 外,扩展运算符(Spread Syntax)也支持迭代器(Iterables

1
[...new Set(['1', '2', '3'])]

stringSet,得到字符串中包含的字符

1
2
3
4
5
6
7
8
let str = 'abcdefghijklmnopqrstuvwxyz'

// Set(26) {'a', 'b', 'c', ...}
new Set(str)

// 等价于

new Set(str[Symbol.iterator]())

ObjectMap,也就是把传统的 JavaScript 映射转换为 Map

1
2
3
4
5
6
let mapping = {
'foo': 'bar'
}

// {'foo' => 'bar'}
new Map(Object.entries(mapping))

类似地,Object 的键的集合可以这样获取

1
2
3
4
5
6
let mapping = {
'foo': 'bar'
}

// {'foo'}
new Set(Object.keys(mapping))

生成器对象到底是一个迭代器还是一个可迭代对象

生成器对象既是迭代器也是可迭代对象,一个良好的迭代即实现了迭代器协议,又实现了可迭代协议,方式就是可迭代协议返回的是自身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var g = function* () {
yield 1
yield 2
yield 3
}()

// 'function',因为有 next() 方法,所以是一个迭代器
typeof g.next

// 'function',因为有 @@iterator 方法,所以是一个迭代器
typeof g[Symbol.iterator]

// true,因为 @@iterator 方法返回它自身(也是一个迭代器)
g[Symbol.iterator]() === g

// [1, 2, 3]
[...g]

总结

  • Iterator 接口的目的就是为所有数据结构提供一种统一访问的机制,用 for-of 实现
  • 一个数据结构只要有 Symbol.iterator 属性,就可以认为是『可遍历的』
  • 实现了可迭代协议的对象称为可迭代对象(Iterables),这种对象可以用 for-of 来遍历,MapSetArraystring 都属于可迭代对象,自定义的对象也可以使用这一机制,成为可迭代对象
    • 可迭代协议,需要实现一个 @@iterator 方法,即在键 [Symbol.iterator] 上提供一个方法,对象被 for-of 调用时,这个方法会被调用,方法应该返回一个迭代器对象(Iterator)用来迭代
    • 简单来说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是『可迭代的』(iterable
  • 实现了迭代器协议的对象称为迭代器对象(Iterator),也就是我们说的迭代器对象
    • 迭代器协议,又称 Iteration Protocol,需要实现一个 next() 方法,每次调用会返回一个包含 value(当前指向的值)和 done(是否已经迭代完成)的对象
    • 简单来说,只需要实现 .next() 方法

参考

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×