带你读红宝书之迭代器模式
为什么需要迭代器模式?
比如我们眼前有一个数组,[1,2,3,4,5],我们可以使用下边的代码中的for循环来轻易的进行循环迭代.
const arr=[1,2,3,4,5]
for(let i=0;i<arr.length;i++){
console.log(arr[i])
}
但其实这种方式存在了以下的隐式成本,我们不能明显感知是因为,我们对数组这种数据结构实在太熟悉了:
- 我们必须知道我们将要进行循环迭代的是一个数组
- 我们在使用递增索引来循环迭代的前提下是已经知道了数组是满足递增索引这种规律模式的
由于上边这两个隐式认知成本,我们可以发现如果我们在不知道数据结构类型和不知道数据的迭代规律,那么我们会根本不知道如何下手去循环这个数据.而迭代器模式j就是来解决这个问题的.可以这么理解,迭代器模式让我们可以用一种更通用的接口标准来循环迭代我们的数据.使用迭代器模式来迭代数据,我们不需要知道数据的详细结构,也不需要去摸清他们的规律,因为迭代器模式一般在一门语言里是一个比较通用的规则,我们只需要了解这部分内容就可以轻松的循环迭代我们的数据.根据迭代器接口的实现方式来看,迭代器模式理论上是可以循环迭代任何数据格式的,具体的看你如何去实现对应的接口.在js的世界里,已经有很大一部分内置数据实现了迭代器接口.其中包括:
- 字符串
- 数组
- 映射
- 集合
- arguments对象
- DOM nodelist(这是一种类数组的数据结构)
如果你要检查你的目标数据类型是否支持默认的迭代器,可以尝试将它的默认迭代器工厂函数暴露出来看看
console.log(obj[Symbol.iterator]); // undefined
通常如果支持默认迭代器,那么暴露出来的值不应该是undefined,如果是undefined,那也说明是不支持默认迭代器的. 如果你的目标数据类型刚好是支持可迭代器协议的,那么你只需要调用这个工厂函数就可以得到一个迭代器,下边也展示一下可迭代协议下的数据的完整迭代过程:
const arr=[1,2,3,4,5]
const it=arr[Symbol.iterator]()
while(true){
const res=it.next()
console.log(res.value)
if(res.done)break;// 通过result中的done属性来判断是否应该终止迭代
}
但是通常我们在悄悄使用迭代器,哈哈哈哈,下边这些原生特性其实在后台隐式调用迭代器工厂函数,并完成迭代;也就是说如果你的数据支持迭代器协议,那么以下特性将可以畅通无阻:
- for-of
- 数组解构
- 拓展操作符 ...
- Array.from()
- 创建集合 new Set([1,2,3,4])
- 创建映射
- Promise.all() Promise.race()
- yield*操作符
可以看到支持迭代器模式,是利大于弊的.
迭代器协议
首先我们来说明一下迭代器,迭代器是在你调用该数据的迭代器工厂函数返回给你的.他就是我们用来迭代数据的关键,在迭代器上有一个api:next.你只要调用一次,就会给你下一个被迭代的值,这个值被叫做IteratorResult;IteratorResult上有两个关键属性:value,done;前者表示该次迭代的值,done则表示是否完成了迭代.由此可知如果我们希望完整迭代我们的数据,那么我们需要源源不断的调用迭代器上的next方法,直到返回的IteratorResult上done属性为true.上边已经有代码展示了完整的迭代,这里就不再贴出代码了.
这里还有一个值得一说的地方,与迭代器形成绑定关系的只是数据的一个引用,并不是一个某个时刻的快照,或者一个完整的副本;如果在迭代过程没有完成的情况下,被迭代数据有了修改行为,那么依然会在迭代器的迭代中提现.
自定义迭代器
既然我们知道了迭代器模式运行的原理,那我们是不是可以自己实现自己的迭代器呢?答案是当然可以.
我们首先有下边这个类,很明显他的实例不是一个可迭代协议的数据格式,而我们的工作就是让他支持可迭代协议
class P {
constructor() {
// 假设这种数据结构可以迭代一百次
this.maxAge = 100
}
}
我们首先需要做的就是实现它的迭代器工厂函数,这个工厂函数返回一个迭代器对象,那么它可以是下边的形状
class P {
constructor() {
// 假设这种数据结构可以迭代一百次
this.maxAge = 100
}
[Symbol.iterator]() {
// 使用闭包来让每个迭代器拥有自己的计数器
let i = 0;
// 返回的这个对象就是迭代器
return {
next: () => {
i++
if (i >= this.maxAge) {
return { value: this.maxAge, done: true }
} else {
return { value: i, done: false }
}
}
}
}
}
我们可以对其进行测试
const p = new P()
const it = p[Symbol.iterator]()
while (true) {
const res = it.next()
if (res.done) break
console.log(res.value);
}
如果你有尝试上边的代码,你会发现的确是正常工作的.我们会发现,要让数据在js世界里变成支持可迭代协议的数据,似乎没有想象中那么复杂.你可以在实现自己的迭代器后,尝试使用原生的特性来测试你的迭代器,比如下边的代码,一般都会预期工作
for (const key of p) {
console.log(key);
}
console.log(new Set([...p]));
提前终止迭代
通常我们想要迭代停止下来,就只有等被迭代对象迭代耗尽为止,但是某些情况下我们拥有更多的选择,不必等对象迭代耗尽,就提前去终止他.那么我们可以在迭代器工厂函数返回的迭代器上添加一个return
方法来达到我们想要的的效果,代码可以是下边的形状
class P {
constructor() {
// 假设这种数据结构可以迭代一百次
this.maxAge = 100
}
[Symbol.iterator]() {
// 使用闭包来让每个迭代器拥有自己的计数器
let i = 0;
// 返回的这个对象就是迭代器
return {
next: () => {
i++
if (i >= this.maxAge) {
return { done: true }
} else {
return { value: i, done: false }
}
},
return(){
// 为了防止next继续工作,我们需要做一些额外的工作
i=this.maxAge+1
// 然后返回一个IteratorResult对象即可
return {done:true}
}
}
}
}
这个return api如果在js的一系列原生特性下是被自动调用的,比如遭遇异常,break等情况就会自行调用来提前终止迭代.
总结
可以看到js迭代器并没有想象中那么复杂,它只是延展了对象的可迭代特性.