在 JavaScript
当中对集合中每个元素进行处理是很常见的操作,比如数组遍历、对象的属性遍历,以往这些操作是通过 for
循环、.forEach
、.map
等方式进行,借由迭代器机制为 Map
、Array
、string
等对象提供了统一的遍历语法,以及更方便的相互转换
在最新的 ES6
当中为方便编写迭代器还提供了生成器(Generator
)语法,迭代器和生成器将迭代的概念直接带入核心语言,并提供了一种机制来自定义 for-of
循环的行为,下面我们就来看看迭代器到底是一种怎样的语法
迭代器
所谓迭代器,其实简单来说就是一个具有 next()
方法的对象,每次调用 next()
方法都必须要返回一个『对象』,被返回对象拥有两个属性(如果返回一个非对象值,则需要抛出一个错误)
done
,布尔值,表示遍历是否结束,如果为true
,则表示迭代器已经超过了可迭代次数,在这种情况下value
的值可以被省略,如果迭代器可以产生序列中的下一个值,则为false
value
,表示当前的值,迭代器可以返回的任何JavaScript
值,在done
为true
的时候可省略
比如我们来看下面这个例子
1 | function createIterator(items) { |
在了解了上面的例子以后,我们就会考虑,那么这样一来我们的迭代器对象是不是就可以进行遍历了呢?,我们来试一下
1 | var iterator = createIterator([1, 2, 3]) |
结果会发现运行报错,那么这就表明我们生成的 iterator
对象并不是 iterable
(可遍历的),那么什么又是可遍历的呢?在 ES6
当中规定,默认的 Iterator
接口部署在数据结构的 Symbol.iterator
属性,或者说一个数据结构只要具有 Symbol.iterator
属性,就可以认为是可遍历的(iterable
),简单来说就是,只要『一种数据结构部署了 Iterator 接口,我们就称这种数据结构是可遍历的(iterable)』,比如下面这个例子
1 | const obj = { |
如果我们直接使用 for-of
遍历一个对象会报错,然而如果我们给该对象添加 Symbol.iterator
属性
1 | const obj = { |
由此可以发现,for-of
遍历的其实是对象的 Symbol.iterator
属性,这里就可以引出我们的迭代器协议
迭代器协议
『迭代器协议』又称生成器协议,该协议定义了什么是迭代器对象,迭代器协议定义了一种标准的方式来产生一个有限或无限序列的值,并且当所有的值都已经被迭代后,就会有一个默认的返回值,一些内置类型都是内置的可迭代类型并且有默认的迭代行为,比如 Array
或者 Map
,另一些类型则不是(比如 Object
,下方会进行介绍)
当使用 for-of
循环遍历某种数据结构时,它会首先调用被遍历集合对象的 Symbol.iterator()
方法,该方法返回一个迭代器对象,它的基本语法为
1 | var myIterator = { |
比如 string
,就是一个内置的可迭代对象
1 | var str = 'hi' |
string
的默认迭代器会一个接一个返回该字符串的字符
1 | const str = 'hi' |
我们也可以通过自己的 @@iterator
方法重新定义迭代行为
1 | var str = new String('hi') |
在对象上实现 Iterator 接口
在上面我们提到过,string
、Array
、TypedArray
、Map
和 Set
是所有内置可迭代对象,因为它们的原型对象都有一个 @@iterator
方法,而针对于对象(Object
)默认是没有 Iterator
接口的,如果我们想让它变为可遍历的,有两种方法
- 一种是在其
[Symbol.iterator]
属性当中实现一个上文所述的next
方法 - 或者像上方基本语法一样,在外部实现
next()
方法,然后在[Symbol.iterator]
当中返回this
也可
那么这里就会存在一个问题,为什么 string
、Array
等对象都有部署 Iterator
接口,而偏偏 Object
没有呢?其实是有两个原因
- 一是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定,然而遍历遍历器是一种线性处理,对于非线性的数据结构,部署遍历器接口,就等于要部署一种线性转换
- 二是对对象部署
Iterator
接口并不是很必要,因为Map
弥补了它的缺陷,又正好有Iteraotr
接口
但是我们可以手动的为对象添加一个 Iterator
接口,比如下面这个实现 50
以内的斐波纳契数列的示例
1 | let obj = { |
模拟实现 for-of
下面我们可以尝试模拟一下 for-of
的实现,简单来说,就是通过 Symbol.iterator
获取迭代器对象,然后使用 while
遍历,当迭代器的 done
为 false
的时候退出循环,因为迭代器对象既然可以被 for-of
遍历,那么它肯定就存在 Symbol.iterator
属性
1 | function forOf(obj, cb) { |
内建迭代器
为了更好的访问对象中的内容,比如有的时候我们仅需要数组中的值,但有的时候不仅需要使用值还需要使用索引,在 ES6
当中为数组,Map
,Set
集合内建了以下三种迭代器
entries()
,返回一个遍历器对象,用来遍历[key, value]
组成的数组,对于数组,键名就是索引值keys()
,返回一个遍历器对象,用来遍历所有的键名values()
,返回一个遍历器对象,用来遍历所有的键值
比如以数组为例
1 | var colors = ['red', 'green', 'blue'] |
Map
类型与数组类似,但是对于 Set
类型需要注意
1 | var colors = new Set(['red', 'green', 'blue']) |
通过上面的例子可以发现,Set
类型的 keys()
和 values()
返回的是相同的迭代器,这也意味着在 Set
这种数据结构中键名与键值相同,这里需要注意一点,每个集合类型都是有一个默认的迭代器的,在 for-of
循环中,如果没有显式指定则使用默认的迭代器,则
- 数组和
Set
集合的默认迭代器是values()
方法 Map
集合的默认迭代器是entries()
方法
这也就是为什么直接 for-of
遍历 Set
和 Map
数据结构,会有不同的数据结构返回,这里有一个小技巧,就是遍历 Map
数据结构的时候可以结合解构赋值来输出想要的格式
1 | const valuess = new Map([['key1', 'value1'], ['key2', 'value2']]) |
生成器对象
生成器对象是由一个 Generator
函数(function*
)返回的,并且它符合可迭代协议和迭代器协议
1 | function* g() { |
生成器函数在执行时能暂停,后面又能从暂停处继续执行,调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器(iterator
)对象,当这个迭代器的 next()
方法被首次调用时,其内的语句会执行到第一个出现 yield
的位置为止,yield
后紧跟迭代器要返回的值,或者如果用的是 yield*
(有星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行),而 next()
方法则会返回一个对象,这个对象包含两个属性 value
和 done
value
属性表示本次yield
表达式的返回值done
属性为布尔类型,表示生成器后续是否还有yield
语句,即生成器函数是否已经执行完毕并返回
这里有一个需要注意的地方,如果在生成器函数当中使用了 return
,会立即结束执行,done
会立即变为 true
1 | function* g() { |
在调用 next()
方法的时候,如果传入了参数,那么这个参数会作为上一条执行的 yield
语句的返回值
1 | function* g() { |
现在可以使用生成器方法重新实现之前的的斐波纳契数列示例
1 | let obj = { |
yield*
yield*
表达式用于委托给另一个 Generator
或可迭代对象,下面是一个简单的示例
1 | function* g1() { |
除了生成器对象这一种可迭代对象,yield*
还可以 yield
其它任意的可迭代对象,比如说数组、字符串、arguments
对象等等
1 | function* g3() { |
生成器也可以接收参数
1 | function* g() { |
Map,Set,String,Array 互相转换
可迭代协议给出了统一的迭代协议,使得不同类型的集合间转换更加方便,以下是一些很方便的转换技巧,比如从 Array
生成 Set
,可用于数组去重
1 | new Set(['1', '2', '3']) |
从 Set
得到 Array
1 | Array.from(new Set(['1', '2', '3'])) // ['1', '2', '3'] |
除了 for-of
外,扩展运算符(Spread Syntax)也支持迭代器(Iterables
)
1 | [...new Set(['1', '2', '3'])] |
从 string
到 Set
,得到字符串中包含的字符
1 | let str = 'abcdefghijklmnopqrstuvwxyz' |
从 Object
到 Map
,也就是把传统的 JavaScript
映射转换为 Map
1 | let mapping = { |
类似地,Object
的键的集合可以这样获取
1 | let mapping = { |
生成器对象到底是一个迭代器还是一个可迭代对象
生成器对象既是迭代器也是可迭代对象,一个良好的迭代即实现了迭代器协议,又实现了可迭代协议,方式就是可迭代协议返回的是自身
1 | var g = function* () { |
总结
Iterator
接口的目的就是为所有数据结构提供一种统一访问的机制,用for-of
实现- 一个数据结构只要有
Symbol.iterator
属性,就可以认为是『可遍历的』 - 实现了可迭代协议的对象称为可迭代对象(
Iterables
),这种对象可以用for-of
来遍历,Map
,Set
,Array
,string
都属于可迭代对象,自定义的对象也可以使用这一机制,成为可迭代对象- 可迭代协议,需要实现一个
@@iterator
方法,即在键[Symbol.iterator]
上提供一个方法,对象被for-of
调用时,这个方法会被调用,方法应该返回一个迭代器对象(Iterator
)用来迭代 - 简单来说,一个数据结构只要具有
Symbol.iterator
属性,就可以认为是『可迭代的』(iterable
)
- 可迭代协议,需要实现一个
- 实现了迭代器协议的对象称为迭代器对象(
Iterator
),也就是我们说的迭代器对象- 迭代器协议,又称
Iteration Protocol
,需要实现一个next()
方法,每次调用会返回一个包含value
(当前指向的值)和done
(是否已经迭代完成)的对象 - 简单来说,只需要实现
.next()
方法
- 迭代器协议,又称