重温JavaScritp(lesson24) 生成器

今天我们要来学习生成器的有关内容。生成器是ES6的新特性。生成器的定义方式是先创建一个函数,调用这个函数再返回生成器对象g。这个生成器对象g是一个可迭代对象,我们可以通过Array.from(g),[…g],for..of循环来使用它。

1.生成器函数的定义

生成器形式是一个函数,function关键字的后面、函数名称的前面加一个星号来表示它是一个生成器,只要是可以定义函数的地方就可以定义生成器。

注意:生成器函数的星号不受两侧的空格影响。这个星号可以紧接着function关键字,可以处在函数名和function关键字之间并用若干空格和两者隔开,也可以在其后紧接着函数名。例如:

function * newName(){}
function* newName(){}
function *newName(){}

如上代码中这三种形式都是等价的。

生成器函数允许我们声明一种特殊的迭代器,这种迭代器会推迟代码的执行,同时保持自己的上下文。下面我来看一下生成器函数的迭代:

2.生成器函数的迭代

function * newName(){
    yield 'new'
    yield 'name'
    yield 'study'
    yield 'JavaScript'
}

const test = newName()
let res1 = typeof test[Symbol.iterator] === 'function'
console.log('res1',res1)
let res2 = typeof test.next === 'function'
console.log('res2',res2)
let res3 = test[Symbol.iterator]() === test
console.log('res3',res3)
console.log(Array.from(test))

之前我们学习过迭代器,每次迭代都会调用next方法从序列中取出一个值。但是在生成器中看不到返回值的next方法,只能看到向序列中添加的yield关键字。

生成器对象同时遵守可迭代协议和迭代器协议,通过如上代码我们可以知道:

(1)生成器对象test是通过生成器函数newName创建的;

(2)生成器对象test是一个可迭代对象,因为它拥有Symbol.iterator属性,此属性是一个方法;

(3)生成器对象test也是一个迭代器,因为它有一个next方法;

(4)生成器对象test的迭代器就是它自己。

以上代码运行结果如下所示:

图片

迭代会触发生成器函数中的副作用。当生成器函数恢复执行以返回序列中下一个元素的时候,每一个yield语句后面的console.log()语句都会执行:

function * newName(){
    yield 'new'
    console.log(1)
    yield 'name'
    console.log(2)
    yield 'study'
    console.log(3)
    yield 'JavaScript'
    console.log(4)
}
console.log([...newName()])

运行效果如下图:

图片

再来看一个使用for...of的例子:

function * newName(){
    yield 'new'
    console.log(1)
    yield 'name'
    console.log(2)
    yield 'study'
    console.log(3)
    yield 'JavaScript'
    console.log(4)
}
for(let item of newName()){
    console.log(item)
}

运行结果如下图:

图片

 

3.使用yield* 委托生成序列

生成器函数可以使用 yield* 将生成序列的任务委托给一个生成器对象或其他可迭代对象。如下代码所示:

function * test() {
    yield* 'hello'
}
console.log([...test()])

运行结果如下图:

图片

 

当然直接使用[…'hello']更简单。然而,有多条yield语句时,委托的作用就体现出来了,如下代码所示:

function * test(name) {
    yield* 'hello '
    yield* name
}
console.log([...test('newName')])

运行结果如下图:

图片

我们可以通过yield* 将生成序列的任务委托给任何遵守可迭代协议的对象,而不仅仅是字符串。如下代码就展示了如何使用yield和yield*,并组合其他生成器函数、可迭代对象和扩展操作符来描述一个值序列:

const test1 = {
    [Symbol.iterator]() {
        const items = ['n','e','w']
        return {
            next: () => ({
                done: items.length === 0,
                value: items.shift()
            })
        }
    }
}
function* test2(num1,num2) {
    yield num1+num2
    yield num2*num2
}
function* test3(){
    yield* test1
    yield 777
    yield* ['new','name']
    yield* [...test2(2,3)]
    yield [...test2(2,3)]
}
console.log([...test3()])

运行结果如下图:

图片

 

除了使用Array.from(g),[…g],for..of的方式来迭代生成器对象,我们也可以直接手工迭代生成器对象:

4.手工迭代生成器

能够手工迭代生成器的原因是,生成器器对象与其他可迭代对象一样,有Symbol.iterator属性,也就可以通过next方法按需取值。

如下代码展示了:创建了生成器newName,以及如何使用生成器对象generatorTest和while循环来手工迭代它:

function * newName(){
    yield 'new'
    yield 'name'
    yield 'study'
    yield 'JavaScript'
}
const generatorTest = newName()
while(true) {
    const item = generatorTest.next()
    if(item.done) {
        break
    }
    console.log(item.value)
}

代码执行结果如下图所示

图片

 

和for…of循环比较而言,用迭代器遍历生成器看起来比较麻烦,但是有一些场景却比较合适。因为for…of是一个同步循环,而有迭代器的话,什么时候调用next()方法就可以由我们控制。

迭代完生成器generatorTest的整个序列后,再调用next()方法不会有什么变化,只会返回{done:true},如下代码所示:

function * newName(){
    yield 'new'
    yield 'name'
    yield 'study'
    yield 'JavaScript'
}
const generatorTest = newName()
while(true) {
    const item = generatorTest.next()
    if(item.done) {
        break
    }
    console.log(item.value)
}
console.log(generatorTest.next())
console.log(generatorTest.next())

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

图片

 

如下代码定义了一个生成无穷斐波那契序列的生成器,然后我们实例化了生成器对象并读取序列中的前3个值:

function *f() {
    let f1 = 0
    let f2 = 1
    while(true) {
        yield f2
        const next = f1 + f2
        f1 = f2
        f2 = next
    }
}
const g = f()
console.log(g.next())
console.log(g.next())
console.log(g.next())

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

图片

 

也可以使用可迭代对象来实现与此相似的效果:

const f = {
    [Symbol.iterator]() {
        let f1 = 0
        let f2 = 1
        return{
            next() {
                const value = f2
                const next = f1 + f2
                f1 = f2
                f2 = next
                return {
                    value,
                    done: false
                }
            }
        }
    }
}
const g = f[Symbol.iterator]()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())

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

图片

 

如果将Symbol.iterator属性改写成为一个生成器函数,那么它照样会工作,如下代码所示:

const f = {
    *[Symbol.iterator]() {
        let f1 = 0
        let f2 = 1
        while(true) {
            yield f2
            const next = f1 + f2
            f1 = f2
            f2 = next
        }
    }
}
const g = f[Symbol.iterator]()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())

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

图片

 

验证可迭代协议的有效性:如下代码中我们使用了for…of 验证了如果symbol.iterator属性是一个生成器的情况。

const f = {
    *[Symbol.iterator]() {
        let f1 = 0
        let f2 = 1
        while(true) {
            yield f2
            const next = f1 + f2
            f1 = f2
            f2 = next
        }
    }
}
for (let item of f) {
    if(item >10) {
        break
    }
    console.log(item)
}

运行结果如下图所示:

图片

 

5.给生成器传参数以及生成器的关闭

可以使用yield实现输入和输出,上一次让生成器函数暂停的yield关键字会接收到传给next方法的第一个值。

需要注意的是第一次调用next方法传入的值不会被使用,因为这一次调用是为了开始执行生成器函数:

function * newName(item){
    console.log(item)
    console.log(yield)
    console.log(yield)
}

let g = newName('new')
g.next('name')
g.next('study')
g.next('JavaScript')
g.next('good')

运行结果如下图所示:

图片

再来看一段代码:

function * newName(){
    return yield 'new'
}

let g = newName()
console.log(g.next())
console.log(g.next('name'))

其运行结果为:

图片

 

因为必须要对整个表达式求值才能确定要返回的值,所以它在遇到yield关键字的时候暂停执行并计算出要返回的值:“new”。下一次调用next传入“name”,作为交给同一个yield的值。然后这个值被确定为本次生成器函数要返回的值。

yield关键值并非只能使用一次,如下代码就多次使用yield定义了一个无穷计数的生成器函数:

// 使用生成器定义一个无穷计数生成器函数
function * generateCount(){
    for(let i=0;;i++){
        yield i
    }
}
let couterGen = generateCount();

console.log(couterGen.next().value)
console.log(couterGen.next().value)
console.log(couterGen.next().value)
console.log(couterGen.next().value)
console.log(couterGen.next().value)
console.log(couterGen.next().value)

运行结果如下图所示:

图片

使用生成器可以实现范围和填充数组:

function* range(start,end) {
    while(end>start){
        yield start++
    }
}
for(const x of range(4,7)) {
    console.log(x)
}

如上代码实现了范围,代码运行效果如下图所示:

图片

如下代码实现了填充数组:

function* zeroes(n) {
    while(n--) {
        yield 0;
    }
}
console.log(Array.from(zeroes(8)))

运行结果如下图所示:

图片

生成器可选的return方法用于提前终止迭代器,还有throw方法也可以强制生成器进入关闭状态。

如下代码调用了生成器的return方法:

function * newName(){
    yield 'new'
    yield 'name'
    yield 'study'
    yield 'JavaScript'
}
const g = newName()
console.log(g.next())
console.log(g.next())
console.log(g.return())
console.log(g.next())

运行结果如下图所示:

图片

使用try/finally块可以避免立即终止迭代序列,因为finally块内的代码会在执行流退出函数前执行,这意味着finally块中的yield表达式会继续回送序列中值。

function * newName(){
    try{
        yield 'new'
    } finally {
        yield 'name'
        yield 'study'
    }
    yield 'JavaScript'
}

const g = newName()
console.log(g.next())
console.log(g.return())
console.log(g.next())

运行结果如下图所示:

图片

 

如果使用扩展操作符、Array.from或者for…of来迭代这个生成器,那么无论return语句放在什么位置,结果中都不会包含返回的value,如下代码所示:

function * newName(){
    yield 'new'
    yield 'name'
    return 'study'
    yield 'JavaScript'
}
for(const item of newName()) {
    console.log(item)
}
console.log([...newName()])
console.log(Array.from(newName()))

运行结果如下图所示:

图片

 

要取生成器返回的值则必须使用next()方法去获取:

function * newName(){
    yield 'new'
    yield 'name'
    return 'study'
    yield 'JavaScript'
}
const g = newName()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())

运行结果如下图所示:

图片

6.生成器的应用

如下代码中定义了树形结构,并定义了一个基于生成器函数的深度优先遍历方法:

// 生成器遍历树形结构
class Node {
    constructor(value,...children) {
        this.value = value
        this.children = children
    }
}
const root = new Node(
    1,
    new Node(2),
    new Node(3,
        new Node(4,
            new Node(5,
                new Node(6)
            ),
            new Node(7)
        )
    ),
    new Node(8,
        new Node(9),
        new Node(10)
    )
)
function* depthFirst(node) {
    yield node.value
    for(const child of node.children) {
        yield* depthFirst(child)
    }
}
console.log([...depthFirst(root)])

在深度优先遍历方法中,返回当前节点的值,然后再迭代其子节点,使用yield*操作符来拼接迭代器递归的结果,返回序列中的每一项。

图片

也可以将上面定义的depthFirst生成器作为迭代器,将node转换为可迭代对象,如下代码所示:

// 生成器遍历树形结构
class Node {
    constructor(value,...children) {
        this.value = value
        this.children = children
    }
    *[Symbol.iterator](){
        yield this.value
        for(const child of this.children) {
            yield* child
        }
    }
}
const root = new Node(
    1,
    new Node(2),
    new Node(3,
        new Node(4,
            new Node(5,
                new Node(6)
            ),
            new Node(7)
        )
    ),
    new Node(8,
        new Node(9),
        new Node(10)
    )
)
console.log([...root])

运行结果如下:

图片

 

如果要使用宽度优先遍历来遍历树的节点,则可以使用队列来保存尚未访问的节点,在遍历的每一步都是先打印当前节点值,并将当前节点的所有子节点放入队列中。

// 生成器遍历树形结构
class Node {
    constructor(value,...children) {
        this.value = value
        this.children = children
    }
    *[Symbol.iterator](){
        const queue = [this]
        while(queue.length) {
            const node = queue.shift()
            yield node.value
            queue.push(...node.children)
        }
    }
}
const root = new Node(
    1,
    new Node(2),
    new Node(3,
        new Node(4,
            new Node(5,
                new Node(6)
            ),
            new Node(7)
        )
    ),
    new Node(8,
        new Node(9),
        new Node(10)
    )
)
console.log([...root])

运行结果为:

图片

也可以使用生成器定义一个计数器,代码如下所示:

// 使用生成器定义一个计数器
function* times(n){
    while(n--) {
        yield
    }
}
for(let item of times(3)) {
    console.log('new name')
}
//输出3次 new name

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

图片

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

重温新知

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

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

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

打赏作者

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

抵扣说明:

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

余额充值