前言
然叔老师一在群里发布代码实现的题目,我就知道要写文章,不写文章难以解释。关于迭代器我并不陌生,大概在16年的时候就有接触过,那时候是花了3个月的时间断断续续的研究设计模式,里面就有一个迭代器模式,当时是个小白,只会面向过程编程,只能通过CV来完成课堂里老师布置的作业。
由于很多概念不懂,于是就在笔记本上抄了一本面向对象的书籍,后面我参加了系里的面向对象程序设计大赛,凭借我的毅力和摸到的一些设计感觉做了一个多功能摇奖机,拿到了特等奖,想想这也算是对我踏入编程世界的启蒙吧。
迭代器是啥嘞,你可以把它当作是for循环包了一层外壳,其实就是用来遍历对象用,当你要遍历某一个对象时,就会用到迭代器了。
设计
设计思路:
要有一个待迭代的数据对象,存放原始数据。
要有一个对迭代器行为进行封装的class,在这个class中可以定义遍历对象的一些行为。
要有一个获取指定数据对象迭代器操作类,在这个操作类中就直接拿到指定的迭代器。当然你在初始化的时候你可以传入待迭代的数据对象,也可以在操作类中追加待迭代的数据对象的子项。
效果与实现
输出结果
代码实现
// 待遍历的对象
const list = [
{
name: '戴沐白'
},
{
name: '朱竹清'
},
{
name: '唐三'
},
{
name: '小舞'
},
{
name: '奥斯卡'
},
{
name: '宁荣荣'
},
{
name: '马红俊'
},
{
name: '白沉香'
}
]
// 用来做测试的迭代器Class
class TestIterator {
constructor(douluodalu) {
this.douluodalu = douluodalu
this.currentLocation = 0
}
hasNext(){
const result = this.douluodalu.entryList.length > this.currentLocation && this.currentLocation > -1;
return result
}
next() {
if (this.currentLocation === this.douluodalu.entryList.length) {
this.currentLocation = 0
}
const result = this.douluodalu.entryList[this.currentLocation]
this.currentLocation++
return result.name // 暂时只取名称,在复杂的情况下,这里可以做更多的操作
}
// 如果超出遍历的范围,才重置
reset () {
// 如果超出列表范围,那么就重置当前指向的位置(重置索引)
if (this.currentLocation === this.douluodalu.entryList.length) {
this.currentLocation = 0;
}
}
// 强制重置
forceReset () {
this.currentLocation = 0;
}
}
// 斗罗大陆的Class
class Douluodalu {
constructor(personnelList) {
this.entryList = personnelList
}
getIterator() {
return new TestIterator(this)
}
}
const douluodaluIterator = new Douluodalu(list).getIterator()
// 遍历第一次
while(douluodaluIterator.hasNext()) {
const personnel = douluodaluIterator.next()
console.log(`斗罗大陆:${personnel}`)
}
console.log(`第一轮点名结束,开启第二轮点名`)
douluodaluIterator.reset()
// 遍历第二次
while(douluodaluIterator.hasNext()) {
const personnel = douluodaluIterator.next()
console.log(`斗罗大陆:${personnel}`)
}
小总结
当我拿到迭代器之后,我就不需要知道要遍历的对象内部结构了,只需要知道该对象是否还能继续遍历下去,可以取到该对象愿意被遍历出来的数据就行。
让待遍历的对象与遍历对象的行为进行解耦,而那个操作类相当于加一层封装,在计算机世界中,遇到复杂的问题时,通过加一层处理机制来解决该问题,迭代器也是如此吧。
小彩蛋
js中的迭代器是怎么运用的呢?
从今天然叔老师今天发的bilibili视频
中可以看出,js中的迭代器通过for of来运用的。
js中这些成员是自带迭代器功能的:
const list = {
"字符串": 'a-b',
"数组": ["数组A","数组B"],
"JS中的Map": new Map([
['a', "a1"],
['b', "b1"],
]),
"JS中的Set": new Set([
'e',
'f',
]),
"JS中的NodeList": document.querySelector('body').childNodes
}
// 输出这些自带迭代器功能的成员
Object.keys(list).forEach(keyName => {
const item = list[keyName]
console.log(`${keyName}: ==========================`)
for(const value of item) {
console.log(value)
}
})
上面为啥不对list使用for of呢?因为list是一个普通的js对象,它默认不支持迭代器功能。所以无法对它进行遍历噢,于是就有下面我们要实现的自制迭代器功能的代码。
给普通js对赋能迭代器功能
js中如果想要一个对象支持for of,那么就得让它拥有一个成员,这个成员就是Symbol.iterator
,那么就来实现这个功能吧。
// 给某个对象赋能,让该对象拥有迭代器的功能
const list = {
"字符串": 'a-b',
"数组": ["数组A","数组B"],
"JS中的Map": new Map([
['a', "a1"],
['b', "b1"],
]),
"JS中的Set": new Set([
'e',
'f',
]),
"JS中的NodeList": document.querySelector('body').childNodes
}
// 给某个对象赋能,让该对象拥有迭代器的功能
const enableIterable = obj => {
const target = obj
// 获取迭代器
const getIterator = () => {
const keyList = Object.keys(target)
let currentIndex = 0
return {
// 是否还有下一项
hasNext () {
return keyList.length > currentIndex
},
// 获取下一项数据
next() {
let value = undefined
const exist = this.hasNext ()
if (exist) {
const keyName = keyList[currentIndex]
value = target[keyName]
currentIndex++
}
return { done: !exist, value };
},
// 重置
reset() {
if (this.hasNext ()) {
currentIndex = 0
}
},
// 强制重置
forceRest() {
currentIndex = 0
}
}
}
// 给这个对象添加上获取迭代器的功能
target[Symbol.iterator] = getIterator
return getIterator // 也可以直接获取迭代器然后使用。
}
// 给list添加迭代器功能之前,会报错:Uncaught TypeError: list is not iterable
//for (const item of list) {
// console.log(item)
//}
enableIterable(list)
// 加了迭代器功能之后,成功遍历
for (const item of list) {
console.log(item)
}
效果如图:
再总结
js中普通的对象是不具备迭代器功能的噢,需要你通过给他实现一个获取迭代器的方法才行。而且js中字符串、数组、map、set、arguments、NodeList默认具备迭代器功能,可以使用for of来遍历它们。感谢然叔的精彩视频哇,然叔老师牛。
继续彩蛋
感谢速冻🐟的提醒,使用生成器也能够实现迭代器的功能,为啥呢,因为生成器函数返回的对象的数据格式天然的适配迭代器对象的格式,如下图:
所以喽,于是就有了接下来的写法
// 给某个对象赋能,让该对象拥有迭代器的功能
const list = {
"字符串": 'a-b',
"数组": ["数组A","数组B"],
"JS中的Map": new Map([
['a', "a1"],
['b', "b1"],
]),
"JS中的Set": new Set([
'e',
'f',
]),
"JS中的NodeList": document.querySelector('body').childNodes
}
// 给某个对象赋能,让该对象拥有迭代器的功能
const enableIterable = obj => {
const target = obj
// 获取迭代器
const getIterator = function* () {
const keyList = Object.keys(target)
for (let i = 0; i < keyList.length; i++) { // 这里对keyList使用for of 也可以
const keyName = keyList[i];
yield target[keyName];
}
}
// 给这个对象添加上获取迭代器的功能
target[Symbol.iterator] = getIterator
return getIterator // 也可以直接获取迭代器然后使用。
}
// 给list添加迭代器功能之前,会报错:Uncaught TypeError: list is not iterable
//for (const item of list) {
// console.log(item)
//}
enableIterable(list)
// 加了迭代器功能之后,成功遍历
for (const item of list) {
console.log(item)
}
效果是一样的噢
最后的总结
由于生成器函数调用后天然的适配迭代器对象格式,所以可以直接拿来用,这样一来就省了你写next函数和内部的判断逻辑了,非常的方便。
无效的终极杀招
看过ruguo的相关文章后,突然想到了一个终极杀招,使用es6中的yield*语法糖之后,是不是就更加简洁了?这个语法糖类似...
这样的语法糖,看终极代码
// 给某个对象赋能,让该对象拥有迭代器的功能
const list = {
"字符串": 'a-b',
"数组": ["数组A","数组B"],
"JS中的Map": new Map([
['a', "a1"],
['b', "b1"],
]),
"JS中的Set": new Set([
'e',
'f',
]),
"JS中的NodeList": document.querySelector('body').childNodes
}
// 给某个对象赋能,让该对象拥有迭代器的功能
const enableIterable = obj => {
const target = obj
// 获取迭代器
const getIterator = function* () {
yield* target;
}
// 给这个对象添加上获取迭代器的功能
target[Symbol.iterator] = getIterator
return getIterator // 也可以直接获取迭代器然后使用。
}
// 给list添加迭代器功能之前,会报错:Uncaught TypeError: list is not iterable
//for (const item of list) {
// console.log(item)
//}
enableIterable(list)
// 你没有看错,调用栈爆了
for (const item of list) {
console.log(item)
}
无效的终极杀招总结
这个终极杀招似乎不行哇,yield* 会去调用一个生成器函数,并且将里面的内容添加到当前的生成器函数中进行迭代。MDN yield*。
原理:由于target实现了Symbol.iterator,那么yield又会去调用Symbol.iterator,于是又会执行一遍yield target,这样一来就陷入了无限的循环当中了,因为yield* 并不会像我之前那样手动排除自己实现的Symbol.iterator的副作用,所以死递归的调用就把调用栈给爆了。
原理类似于下图的效果
如果g1中再来个yield* g2,也会造成同样的死递归效果。如果g1是正常的,就会没事。