1.ECMAScript迭代发展史
1.1 for循环
在以往js中循环迭代往往都是通过for循环来执行的,但是这种迭代模式并不理想。原因如下:
1.迭代前需要知道如何使用数据结构,没有统一的接口标准。
2.通过索引访问数据是基于数组类型特有的,并不适用于所有的数据结构。
1.2 forEach
ES5新增了forEach方法,向通用迭代需求迈进了一步,但是仍然存在问题。
1.只适用于数组,回调比较笨拙,没有办法标识迭代何时终止。
1.3 迭代器模式
迭代器模式:描述了一个方案,即可以把有些结构称为”可迭代对象”,因为它们实现了正式的iterable接口,而且可以通过迭代器iterator消费。迭代器是按需创建的一次性对象,每个迭代器都会关联一个可迭代对象,迭代器会暴漏迭代其可迭代对象的API。迭代器无须了解其关联的可迭代对象的结构,只需要知道如何取得连续的值,这种概念上的分离正是iterable和iterator的强大之处。
2. 可迭代协议
实现Iterable接口要求同时具备两种能力,支持迭代的自我识别能力和创建实现iterator接口的对象的能力,即必须暴漏一个属性(Symbol.iterator)作为迭代器工厂函数,调用这个工厂函数返回一个新的迭代器供其他结构消费。
实现可迭代协议的结构都会自动兼容接受可迭代对象的任何语言特性,在后台会自动调用提供的可迭代对象的工厂函数,创建一个迭代器实现遍历。接收可迭代对象的原生语言特性包含以下几项:
- for-of
- 数组解构
- 扩展运算符
- Array.from()
- 创建集合
- 创建map
- Promise.all()
- Promise.race()
- yield *操作符
3. 迭代器协议
3.1 原理
迭代器是一种一次性使用的对象,用于迭代其关联的可迭代对象,迭代器API使用next()方法在可迭代对象中遍历数据,每次调用next(),都会返回一个IteratorResult对象,包含两个属性done和value,done是一个布尔值,表示是否还可以再次调用next()取得下一个值,value包含可迭代对象的当前值。
3.2 一次性消费
每个迭代器都表示可迭代对象的一次性有序遍历,不同迭代器的实例之间没有联系,只会独立地遍历可迭代对象。
3.3 动态遍历
迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程,如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化。
3.4 自相等
每个以这种方式(Symbol.iterator { return this; })创建的迭代器本身也实现了Iterable接口,Symbol.iterator属性引用的工厂函数会返回相同的迭代器。
let arr = ['foo', 'bar', 'baz']
let iter1 = arr[Symbol.iterator]()
let iter2 = iter1[Symbol.iterator]()
console.log(iter1 === iter2) //true
4. 迭代器提前终止
可以定义return()方法指定在迭代器提前关闭时执行的逻辑,执行迭代的结构在想让迭代器知道它不像遍历到迭代对象耗尽时,就可以关闭迭代器,可能的情况包括:
- for-of循环通过break continue return throw提前退出
- 解构操作未消费所有值
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 }
}
},
return() {
console.log("提前退出")
return { done: true, value: undefined }
}
}
}
}
因为return()方法是可选的,所以并非所有迭代器都是可关闭的。不过仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的,因为调用return不会强制迭代器进入关闭状态,即便如此,return方法还是会被调用。