重温JavaScript(lesson23)迭代器

​今天我们要一起重温一下迭代器的相关知识。在理解迭代器之前,首先我们要明确迭代的含义。在《JavaScript高级程序设计(第4版)》中对迭代是这样定义的:迭代的意思是按照顺序反复执行的一段程序,通常会有明确的终止条件。下面我们通过具体的实例来理解一下:

1.迭代的含义

首先看一段最简单的循环计数的代码:

for(let i=0;i<10;i++) {
    console.log(i)
}

这段代码指定了迭代的次数,每次迭代执行的操作,以及迭代的终止条件。

再来看一个在有序集合上进行迭代的例子:

let book = ['JavaScript高级程序设计','深入理解JavaScript特性','JavaScript学习指南']
for(let i=0;i<book.length;i++) {
    console.log(book[i])
}

我们看到数组可以很好地支持迭代。因为数组有已知的长度,并且数组的每一项都可以通过索引来获得,所以整个数组可以通过递增索引来遍历。

但是其他JS对象的迭代该如何做呢?例如,如下代码创建了书籍和译者之间的一个映射,有需求需要遍历其中的书名或者 书名/译者 键值对,该如何做呢?

let bookInfo = {
    'JavaScript高级程序设计': '李松峰',
    '深入理解JavaScript特性': '李松峰',
    'JavaScript学习指南': '娄佳'
}

当然借助于如下几个api完成如上的需求并不难,但是如果限制不使用这几个api呢?

let bookInfo = {
    'JavaScript高级程序设计': '李松峰',
    '深入理解JavaScript特性': '李松峰',
    'JavaScript学习指南': '娄佳'
}
console.log(Object.entries(bookInfo))
console.log(Object.keys(bookInfo))

console.log(Object.values(bookInfo))

代码运行结果:

图片

此时,使用类似数组的这种迭代方式便不适用了。因为使用这种方式要求具备如下两点条件:

(1)迭代之前需要事先知道如何使用数据结构。因为数组中的每一项都是通过引用(数组,名)取得数组对象,然后再通过[ ]操作符取得特定索引位置上的项。这并不适合所有数据结构。

(2) 这种遍历顺序不是使用数据结构固有的特性,而是通过递增索引的方式来完成的,这并不适用于其他具有隐式顺序的数据结构。如刚刚定义的bookInfo。

那么如何能够让在开发者无须事先知道如何迭代就能实现迭代操作呢?如何将对象转换为可迭代的呢?普通对象要想转换成可迭代对象,必须遵守一个协议:可迭代协议(实现Iterable接口)或者迭代器协议(给对象的Symbol.Iterator属性赋值)。下面我们来详细学习

2.可迭代协议

很多内置的类型都实现了Iterable接口:字符串、数组、map、set等。

《JavaScript高级程序设计(第4版)》中提到:实现Iterable接口要求同时具备两种能力:(1)支持迭代的自我识别(2)创建实现Iterator接口的对象的能力。这意味着必须暴露一个属性作为“默认迭代器”而且这个属性必须使用特殊的Symbol.Iterator属性作为键。这个默认的迭代器属性必须引用i个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。

下面我们通过具体的实例理解如上的内容:

let author = 'new name'
let authorArr = ['new','name']
let authorMap = new Map().set('name1','new').set('name2','name')
let authorSet = new Set().add('new').add('name')

console.log(author[Symbol.iterator])
console.log(authorArr[Symbol.iterator])
console.log(authorMap[Symbol.iterator])
console.log(authorSet[Symbol.iterator])

代码运行结果:

图片

从运行结果图中可以看出以上类型的变量所对应的类型都实现了迭代器工厂函数,而调用这个工厂函数则会生成一个迭代器,如下代码和运行效果图所示:

let author = 'new name'
let authorArr = ['new','name']
let authorMap = new Map().set('name1','new').set('name2','name')
let authorSet = new Set().add('new').add('name')

console.log(author[Symbol.iterator]())
console.log(authorArr[Symbol.iterator]())
console.log(authorMap[Symbol.iterator]())
console.log(authorSet[Symbol.iterator]())

代码运行结果:

图片

注意:在实际编程的时候不需要显示调用迭代器的工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接受可迭代对象的任何语言特性。——《JavaScript高级程序设计(第4版)》

接收可迭代对象的原生语言特性包括:for-of 循环;数组的解构;扩展操作符;Array.from();创建Set;创建Map; Promise.all()接收由期约组成的可迭代对象;Promise.race()接收由期约组成的可迭代对象;yield*操作符,在生成器中使用。

注意:比较常用的是for-of 循环;扩展操作符…;Array.from()。

这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器,下面我来看几个例子:

let authorArr = ['new','name']
// for-of
for(let item of authorArr) {
    console.log(item)
}
// 数组解构
let [name1,name2] = authorArr
console.log(name1)
console.log(name2)
// 扩展操作符
let authorArr2 = [...authorArr] 
console.log(authorArr2)
// Array.from
let authorArr3 = Array.from(authorArr)
console.log(authorArr3)

代码运行结果:

图片

 

3.迭代器协议

如前面所述,普通对象转换成可迭代对象,必须遵守协议,要么是可迭代协议,要么是迭代器协议。迭代器协议:给这个对象的Symbol.iterator属性赋值一个函数。如果这个对象需要迭代,那么每次迭代都会调用赋值给Symbol.iterator的可迭代协议方法。

Symbol.iterator方法必须返回一个对象,该对象必须遵守迭代器协议。这个协议,规定了如何从可迭代对象中取值。根据协议,迭代器有一个next方法,next方法负责在可迭代对象中遍历数据。next方法不接受参数。每次成功调用next都会返回一个IteratorResult对象,次对象包含两个属性:

value:可迭代对象的下一个值(done为false)或者undefined(done为true);

done:是否可以再次调用next方法获取下一个值。

下面我们来看一下具体的例子:

let authorArr = ['new','name']
// Symbol.iterator 对应一个函数;调用这个函数返回迭代器
let iterator = authorArr[Symbol.iterator]();
console.log(iterator)
// 调用迭代器的next方法
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

代码运行结果:

图片

这里通过创建迭代器,并调用next()方法按顺序迭代了数组,直到不再产生新的值。迭代器并不知道怎么从迭代对象中取得下一个值,也不知道可迭代对象有多大。

 

另外需要注意几点:

第一,只要IteratorResult对象的done属性值为true之后,那么后续调用next就一直返回同样的值了,如代码和下图所示:

let authorArr = ['new','name']
// Symbol.iterator 对应一个函数;调用这个函数返回迭代器
let iterator = authorArr[Symbol.iterator]();
console.log(iterator)
// 调用迭代器的next方法
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

代码运行结果:

图片

第二,每个迭代器都表示对可迭代对象的一次性有序遍历。不同迭代器的实例之间相互不影响。如代码和下图所示:

 

let authorArr = ['new','name']
// Symbol.iterator 对应一个函数;调用这个函数返回迭代器
let iterator = authorArr[Symbol.iterator]();
let iterator2 = authorArr[Symbol.iterator]();
console.log('iterator:',iterator)
console.log('iterator2:',iterator2)
// 调用迭代器的next方法
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator2.next())
console.log(iterator2.next())
console.log(iterator2.next())

代码运行结果:

图片

第三,如果迭代对象在迭代期间被修改了,那么迭代器也会反应出相应的变化来。如下代码所示:

let authorArr = ['new','name']
// Symbol.iterator 对应一个函数;调用这个函数返回迭代器
let iterator = authorArr[Symbol.iterator]();
console.log('iterator:',iterator)
// 调用迭代器的next方法
console.log(iterator.next())
console.log(iterator.next())
authorArr.push('study','JavaScript')
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

代码运行结果:

图片

下面我们看一下,使用Symbol.iterator自定义迭代器的例子:

 

4.自定义迭代器

首先看一个计数器的例子:

class Conunter {
    constructor(max) {
        this.max = max;
    }
    [Symbol.iterator]() {
        let count=1;
        let max = this.max;
        return {
            next() {
                if(count<=max) {
                    return {
                        done: false,
                        value: count++
                    }
                } else {
                    return {
                        done: true,
                        value: undefined
                    }
                }
            }
        }
    }
}
let counter = new Conunter(3);
for(let item of counter) {
    console.log(item)
}

代码运行结果如下图所示:

图片

 

在如上代码中,Counter类的Symbol.iterator属性引用的工厂函数返回了一个迭代器,迭代器的next()方法中定义了遍历计数器的具体实现。

接下来再看一个例子:如果有需求要实现一个可以生成0-1范围内随机数的无穷序列的迭代器,那么可以定义一个无穷序列的对象:

const random = {
    [Symbol.iterator]: () => ({
        next: () =>({value:Math.random(),done: false})
    })
}
const [one,two] = random
console.log(one)
console.log(two)

如上代码所示,我们定义了无穷序列的对象,其Symbol.iterator属性定义了迭代器的工厂函数,工厂函数返回了一个迭代器对象,其next()属性方法中返回的对象永远都是done属性为false的。我们又使用了解构来获取无穷序列的前两个值。代码运行结果如下图所示:

图片

无穷序列不适合大量取值,如果要在满足一定条件下取值,比如取出前i个值的时候,那么可以使用for-of并结合定义一个中断条件来避免无限循环。如下代码所示:

const random = {
    [Symbol.iterator]: () => ({
        next: () =>({value:Math.random(),done: false})
    })
}
for(let item of random) {
    if(item >0.8) {
        break;
    }
    console.log(item)
}

不过这样的代码回来来看是比较难以理解的,因为很多代码都在处理迭代序列,打印随机数,还有个最大值作为条件。所以,可以将其中一部分的逻辑抽象到另一个方法中可回忆使代码更加好理解。

如下代码展示了take方法,此方法接收一个序列,并从序列中取出count个值:

function take(sequence,count){
    return{
        [Symbol.iterator]() {
            const iterator = sequence[Symbol.iterator]()
            return {
                next() {
                    if(count--<1) {
                        return {
                            value: undefined,
                            done: true
                        }
                    } 
                    return iterator.next()
                }
            }
        }
    }
}
let arr = [...take(random,2)]
console.log(arr)
//  [0.9615570096387576, 0.6839646333717435]

下面我们再回过头来看一下迭代对象以生成键值对的例子:

let bookInfo = {
    'JavaScript高级程序设计': '李松峰',
    '深入理解JavaScript特性': '李松峰',
    'JavaScript学习指南': '娄佳',
    [Symbol.iterator](){
        const keys = Object.keys(bookInfo) 
        return {
            next() {
                const done = keys.length === 0
                const key = keys.shift()
                return {
                    done,
                    value: [key,bookInfo[key]]
                }
            }
        }
    }
}
let res = [...bookInfo]
console.log(res)

代码运行结果:

图片

在如上代码中,我们定义了bookInfo对象的Symbol.iterator属性作为一个迭代器工厂函数返回一个迭代器对象,迭代器对象的next方法中获取了当前对象的键值对并返回。

在这段代码中仍然存在一个问题:就是获取对象键值对的代码不应该和某个对象捆绑在一起,使代码耦合性过强。那么我们可以单独写一个方法,将对象作为参数传入,如下代码所示:

function keyValueIterable(target) {
    target[Symbol.iterator] = function() {
        const keys = Object.keys(target)
        return {
            next() {
                const done = keys.length ===0
                const key = keys.shift()
                return {
                    done,
                    value: [key,target[key]]
                }
            }
        }
    }
}
let bookInfo = {
    'JavaScript高级程序设计': '李松峰',
    '深入理解JavaScript特性': '李松峰',
    'JavaScript学习指南': '娄佳'
}
keyValueIterable(bookInfo)
let res = [...bookInfo]
console.log(res)

5.迭代器的终止、错误与异常

除了next()方法之外,迭代器对象还有return()和throw()方法,这些是可选的。

return()方法用于指定迭代器提前关闭时执行的逻辑,它必须返回一个有效的IteratorResult对象。简单情况下可以返回一个{done:true}。我们来看一个例子:

const random = {
    [Symbol.iterator]: () => ({
        next: () =>({value:Math.random(),done: false}),
        return:() => {
            console.log('我被提前终止了')
            return {done: true}
        }
    })
}
for(let item of random) {
    if(item >0.8) {
        break;
    }
    console.log(item)
}

代码一次随机运行的结果如下图所示:

图片

 

今天我们要一起学习的内容就这些,下次我们将一起学习生成器。来一张图总结回顾今日的内容:

图片

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

重温新知

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值