迭代器和生成器
迭代:按照顺序反复多次执行一段程序,通常都会有明确的终止条件。
ES6新增了两个高级特性:迭代器和生成器。
循环是迭代机制的基础。
以前的迭代是用计数循环进行迭代。
for(let i=0;i<10;i++){
console.log(i)
}
缺点:
- 迭代之前需要事先知道如何使用数据结构,并不适合所有数据结构
- 遍历顺序并不是数据结构固有的。
迭代器模式:有些结构实现了正式的Iterable接口,而且可以通过迭代器Iterator消费。
可迭代协议
在js中,需要用Symbol.iterator作为键,值为一个迭代器的工厂函数,调用这个工厂函数必须返回一个新迭代器。
接收可迭代对象的原生语言特性包括:
- for-of 循环
- 数组解构
- 扩展操作符
- Array.from()
- 创建集合
- 创建映射
- Promise.all() 接收由期约组成的可迭代对象
- Promise.race() 接收由期约组成的可迭代对象
- yield* 操作符
这些原生语言结构会在后台调用可迭代对象提供的工厂函数,创建一个迭代器。
迭代器协议
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next方法在可迭代对象中遍历数据。每次调用next(),都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。
下面来看一下IteratorResult的数据结构
interface IteratorResult{
done: boolean,
value: any
}
当done为true时,代表迭代耗尽,说明迭代完毕了。
下面看一个例子
// 创建一个可迭代对象
let arr = ['foo','bar']
// 获取一个迭代器, 调用迭代器工厂函数即可
let iter = arr[Symbol.iterator]()
// 执行迭代
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
结果
如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化;
ps:迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。
显式的创建一个可迭代对象并调用迭代器迭代:
/**
* 显式的迭代器实现
*/
class Foo{
[Symbol.iterator](){
return {
next(){
return {
done: false,
value: 'foo'
}
}
}
}
}
let f = new Foo()
const iter1 = f[Symbol.iterator]()
console.log(iter1.next())
console.log(iter1.next())
console.log(iter1.next())
console.log(iter1.next())
自定义迭代器
上面说了一些迭代器的基础知识,下面来实现一个自定义的迭代器。
// 1. 实现iterator
// 2. 返回一个迭代器对象
// 3. 有next函数
// 4. 如果使用语言特性,后台会自动调用对应函数
class Counter{
constructor(limit) {
this.count = 0
this.limit = limit
}
next(){
if(this.count <= this.limit){
return {
done: false,
value: this.count++
}
}else{
return {
done: true,
value: undefined
}
}
}
[Symbol.iterator](){
return this
}
}
let counter = new Counter(3)
for (let i of counter){
console.log(i)
}
for (let i of counter){
console.log(i)
}
这个例子我们就实现了一个自定义的迭代器,但是缺点在于它只能迭代一次,我们将这个例子进行改造。
可以通过必包将count包裹进去,解决这个问题。
// 1. 实现iterator
// 2. 返回一个迭代器对象
// 3. 有next函数
// 4. 如果使用语言特性,后台会自动调用对应函数
class Counter{
constructor(limit) {
this.count = 0
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}
}
}
}
}
}
let counter = new Counter(3)
for (let i of counter){
console.log(i)
}
for (let i of counter){
console.log(i)
}
结果
提前终止迭代器
return方法用于指定在迭代器提前关闭时执行的逻辑。
// 1. 实现iterator
// 2. 返回一个迭代器对象
// 3. 有next函数
// 4. 如果使用语言特性,后台会自动调用对应函数
class Counter{
constructor(limit) {
this.count = 0
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')
return {done: true}
}
}
}
}
let counter = new Counter(3)
for (let i of counter){
if(i === 2){
break
}
console.log(i)
}
for (let i of counter){
console.log(i)
}
结果:
生成器
生成器拥有在一个函数块内暂停和恢复代码执行的能力。
基础
生成器是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。
function* gen(){}
调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行状态。并且生成器实现了Iterator接口,具有next方法。调用这个方法会让生成器开始或恢复执行。
function* genertaofn(){
console.log('123')
}
const g = genertaofn()
console.log(g)
console.log(g.next)
通过yield中断执行
yield关键字可以让生成器停止和开始执行。
当调用next() 生成器函数在遇到yield关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。 停止执行的生成器函数只能通过在生成器对象上调用next方法来恢复执行。
看一个例子
function* genertaofn(){
yield 'theShy'
console.log('123')
yield '帅气'
}
const g = genertaofn()
console.log(g.next())
console.log(g.next())
console.log(g.next())
使用场景
生成器对象作为可迭代对象
function* nTimes(n){
while (n--){
yield;
}
}
for(let _ of nTimes(3)){
console.log('_')
}
可以通过n控制迭代次数
使用yield实现 输入和输出
yield关键字还可以作为函数的中间参数使用。 yield可以接收next函数的值,但是第一次调用next的值不会有,因为是为了启动迭代器。
function* generatorFn(initValue){
console.log(initValue)
console.log(yield)
console.log(yield)
}
const g = generatorFn('lhkl')
g.next()
g.next('lll')
g.next(3)
g.next(3)
产生可迭代对象
可以使用星号增强yield行为,让他能够迭代一个可迭代对象,从而一次产出一个值。
function* generatorFn(initValue){
yield* [1,2,3]
yield 3
yield 'kkk'
}
const g = generatorFn('lhkl')
// g.next()
// console.log(g.next('lll'))
// g.next(3)
// g.next(3)
for(const item of g){
console.log(item)
}
使用yield实现递归
function* nTimes(n){
debugger
if(n > 0){
yield* nTimes(n-1)
yield n-1
}
}
const g = nTimes(3)
for(let num of g){
console.log('nums==',num)
}
yield* 递归了一个nTimes可迭代对象,当对象迭代完毕之后,进入回溯阶段, 所以结果为0,1,2
提前终止生成器
生成器有next,return还有一个throw
return 方法会强制生成器进入关闭状态。提供给return()方法的值,就是终止迭代器对象的值:
function* gen(){
for(const x of [1,2,3]){
yield x;
}
}
const g = gen()
console.log(g.next())
console.log(g.return(4))
console.log(g.next())
console.log(g.next())
throw在暂停的时候将一个提供的错误注入到生成器对象中,如果错误未被处理,生成器就会关闭。
function* gen(){
for(const x of [1,2,3]){
yield x;
}
}
const g = gen()
console.log(g)
console.log(g.next())
g.throw('123')
console.log(g.next())
console.log(g.next())
如果生成器函数内部处理了这个错误,生成器不会关闭,可以恢复执行,错误处理会跳过对应的yield
function* gen(){
for(const x of [1,2,3]){
try {
yield x;
}catch (e) {
console.log(e)
}
}
}
const g = gen()
console.log(g)
console.log(g.next())
try {
g.throw('123')
}catch (e) {
console.log(e)
}
console.log(g.next())
console.log(g.next())
总结
迭代器是一个可以由任务对象实现的接口,支持连续获取对象产出的每一个值。任何实现Iterable接口的对象有一个Symbol.Iterable属性。
迭代器必须连续通过next()方法才能连续取值,这个方法返回一个IteratorObject。这个对象包含一个done属性和一个value属性。前者是一个布尔值,表示是否还有更多值可以访问;后者表示迭代器返回的当前值。还可以通过js语言特性消费,如for of。
生成器是一种特殊的函数,调用会返回一个生成器对象。这个对象天然实现了Iterable接口,所以可以进行迭代。还支持yield关键字,这个关键字能够暂停执行生成器函数。使用yiele关键字可以通过next()方法接收输入和产生输出。加上*后,yield关键字可以将跟他后面的可迭代任意对象(包括函数本身,这也是实现递归的一种方式)序列化为一串值。