什么?你还不了解前端高级特性:生成器和迭代器?

92 阅读6分钟

迭代器和生成器

迭代:按照顺序反复多次执行一段程序,通常都会有明确的终止条件。

ES6新增了两个高级特性:迭代器和生成器。

循环是迭代机制的基础。

以前的迭代是用计数循环进行迭代。

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

缺点:

  1. 迭代之前需要事先知道如何使用数据结构,并不适合所有数据结构
  2. 遍历顺序并不是数据结构固有的。

迭代器模式:有些结构实现了正式的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关键字可以将跟他后面的可迭代任意对象(包括函数本身,这也是实现递归的一种方式)序列化为一串值。