JavaScript的Iterator和for...of循环
深入了解Symbol,比如Symbol.toStringTag
,Symbol.toPrimitive
等其他Symbol属性可以看看我之前的文章:
Iterator(遍历器)
迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
Iterator的概念
遍历器(Iterator)就是一种统一的接口机制,用来处理遍历所有不同的数据结构。 任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterable
是指有[Symbol.iterator]
属性的对象,这个属性obj[Symbol.iterator]
就是 Iterator(迭代器)。也可以说可迭代对象是实现了Symbol.iterator
方法的对象。凡是部署了
Symbol.iterator
属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。如果
Symbol.iterator
方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。 遍历器对象的根本特征就是具有next
方法。
原生具备Iterator接口的数据结构
- Array
- Set
- Map
- String
- TypeArray: 类似于数组的结构,对底层的二进制进行描述的一种数据结构
- 函数的argiuments对象
- NodeList对象
以上数据类型,都有 Symbol.iterator
属性,属性值是一个函数,执行这个函数就会返回一个迭代器。这个迭代器就有 next
方法可顺序迭代子元素。
判断某个对象是否可迭代:
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
Iterator 的作用
- 迭代器Iterator是一种接口机制,为各种不同的数据结构提供统一的访问机制
- 主要就是供
for...of..
使用 - 使数据结构的成员按照某种顺序排列
- 用一句话总结就是:让不可迭代的数据结构变得可迭代
Iterator 的遍历过程
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的
next
方法,可以将指针指向数据结构的第一个成员。 - 第二次调用指针对象的
next
方法,指针就指向数据结构的第二个成员。 - 不断调用指针对象的
next
方法,直到它指向数据结构的结束位置。
每一次调用next
方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 value
和 done
两个属性的对象。其中, value
属性是当前成员的值, done
属性是一个布尔值,表示遍历是否结束。
模拟next()
实现一个遍历器
注意:Iterator 只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象:
1 | function idMaker() { |
使用ES6的class语法模拟
1 | class MakeIterator { |
使用function来模拟
1 | //自己定义的遍历器生成函数 |
使用Generator函数来模拟
Symbol.iterator()
方法的最简单实现:
1 | let myIterable = { |
遍历器对象的 return(),throw()
遍历器对象除了具有
next()
方法,还可以具有return()
方法和throw()
方法。如果自己写遍历器对象生成函数,那么
next()
方法是必须部署的,return()
方法和throw()
方法是否部署是可选的。
return()
方法的使用场合是,如果for...of
循环提前退出(通常是因为出错,或者有break
语句),就会调用return()
方法。 注意 ,return()方法必须返回一个对象 ,这是 Generator 语法决定的。 如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return()
方法。throw()
方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。
1 | function readLinesSync(file) { |
可迭代协议和迭代器协议
通俗来理解,具有
Symbol.iterator
的就是遵循的可迭代协议。凡是按照next()
调用这种就都可以称之为迭代器协议。
迭代器协议(iterator protocol) :
iterator
协议定义了产生value
序列的一种标准方法。只要实现符合要求的next
函数,该对象就是一个迭代器。相当遍历数据结构元素的指针,类似数据库中的游标。- 在 JavaScript 中迭代器是一个对象,它提供了一个
next()
方法,用来返回序列中的下一项。这个方法返回包含两个属性:done
和value
。
- 在 JavaScript 中迭代器是一个对象,它提供了一个
可迭代协议(iterable protocol) : 一旦支持可迭代协议,意味着该对象可以用
for-of
来遍历,可以用来定义或者定制 JS 对象的迭代行为。常见的内建类型比如Array & Map都是支持可迭代协议的。对象必须实现@@iterator
方法,意味着对象必须有一个带有@@iterator key
的可以通过常量Symbol.iterator
访问到的属性。
将一段不可遍历的数据结构变得可遍历:
可以用Generator函数进行改造:
实现一个迭代器
基于迭代器协议
1 | // 实现 |
基于可迭代协议
实现了生成迭代器方法的对象称为 可迭代对象 也就是说这个对象中包含一个方法, 该方法返回一个迭代器对象
一般使用 Symbol.iterator
来定义该属性, 学名叫做 @@iterator
方法
1 | // 一个可迭代对象需要具有[Symbol.iterator]方法,并且这个方法返回一个迭代器 |
基于生成器Generator
1 | // 生成器 |
调用 Iterator 接口的场合
解构赋值
1 | let set = new Set().add('a').add('b').add('c'); |
扩展运算符
1 | // 例一 |
实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。即let arr=[...iterable];
yield*
1 | let generator = function* () { |
其他场合
for...of
Array.from()
Map()
,Set()
,WeakMap()
,WeakSet()
(比如new Map([['a',1],['b',2]])
)Promise.all()
Promise.race()
for...of...
循环
1 | function each(data){ |
ES6 借鉴 C++、Java、C# 和 Python 语言,引入了 for…of 循环,作为遍历所有数据结构的统一的方法。一个数据结构只要部署了 Symbol.iterator
属性,就被视为具有 iterator
接口,就可以用 for...of
循环遍历它的成员。
- Array
- Set
- Map
- String
- TypeArray:类似于数组的结构,对底层的二进制进行描述的一种数据结构
- 函数的argiuments对象
- NodeList对象
- Generator 对象
以上这些可以直接使用for...of
循环。
除此之外,其他数据结构(主要是对象)的 Iterator
接口,都需要自己在Symbol.iterator
属性上面部署,这样才会被for...of
循环遍历。
数组
数组原生具备 iterator
接口(即默认部署了 Symbol.iterator
属性) ,for…of 循环本质上就是调用这个接口产生的遍历器。
1 | let arr = ['red','green','blue']; |
注意: for...of
循环 调用遍历器接口,数组的遍历器接口 只返回具有数字索引的属性 。这一点跟 for...in
循环不一样。
1 | let arr = [1,2,3]; |
Set 和 Map 结构
Set 和 Map 结构也原生具有 Iterator
接口,可以直接使用 for...of
循环。
- 遍历的顺序是按照各个成员被添加进数据结构的顺序。
- Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
1 | // 遍历 Set 结构 |
1 | // 遍历 Map 结构 |
计算生成的数据结构
entries()
*返回一个遍历器对象,用来遍历*[键名, 键值]
组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的Iterator
接口,默认就是调用entries
方法。keys()
返回一个遍历器对象,用来遍历所有的键名。values()
返回一个遍历器对象,用来遍历所有的键值。
1 | let arr = ['a', 'b', 'c']; |
类似数组的对象
for...of
循环用于字符串、DOM NodeList 对象、arguments 对象的例子:
1 | // 字符串 |
注意:并不是所有类似数组的对象(存在数值键名和length
属性)都具有 Iterator 接口:
1 | // 这是一个类似数组的对象,但是它没有 Iterator 接口 |
可以对这些类似数组的对象部署 Iterator 接口,有一个简便方法,就是 Symbol.iterator
方法直接引用数组的 Iterator 接口,例如:(NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。)
1 | NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; |
注意:类数组对象才会有Symbol.iterator
方法,普通对象部署数组的 Symbol.iterator
方法,并无效果。
1 | let iterable = { |
1 | let iterable = { |
对于普通的对象,for…of 结构不能直接使用
对于普通的对象,for...of
结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。
一个对象如果要具备可被 for...of
循环调用的 Iterator 接口,就必须在Symbol.iterator
的属性上部署遍历器生成方法( 原型链上的对象具有该方法也可 ) 。
1 | class RangeIterator { |
1 | let obj = { |
1 | function* entries(obj) { |
案例:
1 | // 想要通过 for of 循环直接遍历出 price 对象中 a、b、c 三个属性对应数组的最小值 |
与其他遍历语法的比较
for循环
1 | for (let index = 0; index < myArray.length; index++) { |
数组的forEach方法
这种写法的问题在于,无法中途跳出forEach循环,break
命令或return
命令都不能奏效。
1 | myArray.forEach(function (value) { |
for...in...
循环
for...in
循环可以遍历数组的键名。for...in
循环主要是为遍历对象而设计的,不适用于遍历数组。
1 | for (let index in myArray) { |
for...in
循环有几个缺点:
- 数组的键名是数字,但是
for...in
循环是以字符串作为键名的,“0”、“1”、“2”等等。 for...in
循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。- 某些情况下,
for...in
循环会以 任意顺序 遍历键名。