【ES6基础】生成器(Generator)

在上一篇文章里《【ES6基础】迭代器(iterator)》,笔者介绍了迭代器及相关实例,咱们要实现一个迭代器要写很多的代码。幸运的是,ES6引入了一个新的函数类型——生成器函数(Generator function),让咱们可以更轻松更便捷的实现迭代器的相关功能。javascript

今天笔者将从如下几个方面进行介绍生成器(Generator):css

  • 什么是生成器
  • 生成器的基本语法
  • yield关键字
  • 生成器函数的类型检测
  • yield*委托
  • return(value)方法
  • throw(exception)方法
  • 向生成器传递数据
  • 生成器示例应用

本篇文章阅读时间预计15分钟前端

什么是生成器?

生成器第一次出如今CLU语言中。CLU语言是美国麻省理工大学的Barbara Liskov教授和她的学生们在1974年至1975年间所设计和开发出来的。Python、C#和Ruby等语言都受到其影响,实现了生成器的特性,生成器在CLU和C#语言中被称为迭代器(iterator),Ruby语言中称为枚举器(Enumerator)。java

生成器的主要功能是:经过一段程序,持续迭代或枚举出符合某个公式或算法的有序数列中的元素。这个程序即是用于实现这个公式或算法的,而不须要将目标数列完整写出。git

在ES6定义的生成器函数有别于普通的函数,生成器能够在执行当中暂停自身,能够当即恢复执行也能够过一段时间以后恢复执行。最大的区别就是它并不像普通函数那样保证运行到完毕。还有一点就是,在执行当中每次暂停或恢复循环都提供了一个双向信息传递的机会,生成器能够返回一个值,恢复它的控制代码也可发回一个值。算法

生成器的基本语法

与普通函数语法的差异,在function关键字和函数名直接有个*号,这个*做为生成器函数的主要标识符,以下所示:编程

function *it(){}复制代码

*号的位置没有严格规定,只要在中间就行,你能够这么写:json

function *it(){ }
function* it(){ }
function * it(){ }
function*it(){ }复制代码

笔者以为*靠近函数名——function *it(){ },看着更为清晰,选择哪一种书写方式彻底凭我的喜爱。数组

调用生成器也十分简单,就和调用普通函数同样,好比:bash

it();复制代码

同时也能够向生成器函数传递参数:

function *it(x,y){ 

}
it(5,10);复制代码

yield关键字

生成器函数中,有一个特殊的新关键字:yield——用来标注暂停点,以下段代码所示:

function *generator_function(){ 
  yield 1; 
  yield 2; 
  yield 3;
}复制代码

如何运行生成器呢?以下段代码所示:

let generator = generator_function();
console.log(generator.next().value);//1
console.log(generator.next().value);//2
console.log(generator.next().value);//3
console.log(generator.next().done);//true

generator = generator_function();
let iterable = generator[Symbol.iterator]();
console.log(iterable.next().value);//1
console.log(iterable.next().value);//2
console.log(iterable.next().value);//3
console.log(iterable.next().done);//true复制代码

从上述代码咱们能够看出:咱们能够在实例化的生成器generator的对象里直接调用next()方法,同时咱们也能够调用生成器原型链的Symbol.iterator属性方法调用next(),效果是一致的。咱们每调用一次next()方法,就是顺序在对应的yield关键字的位置暂停,遵照迭代器协议,返回例如这样形式的对象: {value:”1″,done:false},直到全部的yield的值消费完为止,再一次调用next()方法生成器函数会返回 {value:undefined,done:true},说明生成器的全部值已消费完。因而可知done属性用来标识生成器序列是否消费完了。当done属性为true时,咱们就应该中止调用生成器实例的next方法。还有一点须要说明带有yield的生成器都是以惰性求值的顺序执行,当咱们须要时,对应的值才会被计算出来。

生成器函数的类型检测

如何检测一个函数是生成器函数和生成器实例的原型呢,咱们可使用constructor.prototype属性检测,实例代码以下:

function *genFn() {}
const gen=genFn();
console.log(genFn.constructor.prototype);//GeneratorFunction {}
console.log(gen.constructor.prototype);//Object [Generator] {}
console.log(gen instanceof genFn)//true
//判断某个对象是否为指定生成函数所对应的实例复制代码

除了以上方法进行判断,咱们还可使用@@tostringTag属性,以下段代码所示:

function *genFn() {}
const gen=genFn();
console.log(genFn[Symbol.toStringTag]);//GeneratorFunction
console.log(gen[Symbol.toStringTag]);//Generator复制代码

yield*委托

yield* 能够将可迭代的对象iterable放在一个生成器里,生成器函数运行到yield * 位置时,将控制权委托给这个迭代器,直到耗尽为止,以下段代码所示:

function *generator_function_1(){ 
 yield 2; 
 yield 3;
}
function *generator_function_2(){
 yield 1; 
 yield* generator_function_1(); 
 yield* [4, 5];
}
const generator = generator_function_2();
console.log(generator.next().value); //1
console.log(generator.next().value); //2
console.log(generator.next().value); //3
console.log(generator.next().value); //4
console.log(generator.next().value); //5
console.log(generator.next().done);  //true复制代码

从上述代码中,咱们在一个生成器中嵌套了一个生成器和一个数组,当程序运行至生成器generator_function_1()时,将其中的值消费完跳出后,再去迭代消费数组,消费完后,done的属性值返回true。

return(value)方法

你能够在生成器里使用return(value)方法,随时终止生成器,以下段代码所示:

function *generator_function(){ 
 yield 1; 
 yield 2; 
 yield 3;
}
const generator = generator_function();
console.log(generator.next().value); //1
console.log(generator.return(22).value); //22
console.log(generator.next().done);//true复制代码

从上述代码咱们看出,使用return()方法咱们提早终止了生成器,返回return里的值,再次调用next()方法时,done属性的值为true,因而可知return提早终止了生成器,其余的值也再也不返回。

throw(exception)方法

除了用return(value)方法能够终止生成生成器,咱们还能够调用 throw(exception) 进行提早终止生成器,示例代码以下:

function *generator_function(){ 
    yield 1;
    yield 2;
    yield 3;    
}
const generator = generator_function();
console.log(generator.next());
try{
    generator.throw("wow");
}
catch(err){
    console.log(err);
}
finally{
    console.log("clean")
}
console.log(generator.next());复制代码

上段代码输出:

{ value: 1, done: false }
wow
clean
{ value: undefined, done: true }复制代码

由此能够看出,在生成器外部调用try…catch…finally,throw()异常被try…catch捕捉并返回,并执行了finally代码块中的代码,再次调用next方法,done属性返回true,说明生成器已被终止,提早消费完毕。

咱们不只能够在next执行过程当中插入throw()语句,咱们还能够在生成器内部插入try…catch进行错误处理,代码以下所示:

function *generator_function(){ 
try { 
 yield 1; 
} catch(e) { 
 console.log("1st Exception"); 
} 
try { 
 yield 2; 
} catch(e) { 
 console.log("2nd Exception"); 
}
}
const generator = generator_function();
console.log(generator.next().value);
console.log(generator.throw("exception string").value);
console.log(generator.throw("exception string").done);复制代码

运行上段代码将会输出:

1
1st Exception
2
2nd Exception
true复制代码

从代码输出能够输出,当咱们在generator.throw()方法时,被生成器内部上个暂停点的异常处理代码所捕获,同时能够继续返回下个暂停点的值。因而可知在生成器内部使用try…catch能够捕获异常,并不影响值的下次消费,遇到异常不会终止。

向生成器传递数据

生成器不但能对外输出数据,同时咱们也能够向生成器内部传递数据,是否是很神奇呢,仍是从一段代码开始提及:

function *generator_function(){ 
    const a = yield 12;
    const b = yield a + 1;
    const c = yield b + 2; 
    yield c + 3; // Final Line
}
const generator = generator_function();
console.log(generator.next().value);
console.log(generator.next(5).value);
console.log(generator.next(11).value);
console.log(generator.next(78).value);
console.log(generator.next().done);复制代码

运行上述代码将会输出:

12
6
13
81
true复制代码

从上述代码咱们能够看出:

  • 第一次调用generator.next(),调用yield 12,并返回值12,至关启动生成器。并在 yield 12 处暂停。
  • 第二次调用咱们向其进行传值generator.next(5),前一个yield 12这行暂停点获取传值,并将5赋值给a, 忽略12这个值,而后运行至 yield (a + 1) 这个暂停点,所以是6,并返回给value属性。并在 yield a + 1 这行暂停。
  • 第三次调用next,同理在第二处暂停进行恢复复,把11的值赋值给b,忽略a+1运算,所以在yield b + 2中,返回13,并在此行暂停。
  • 第四次调用next,函数运行到最后一行,C变量被赋值78,最后一行为加法运算,所以value属性返回81。
  • 再次运行next()方法,done属性返回true,生成器数值消费完毕。

从上述步骤说明中,向生成器传递数据,首行的next方法是启动生成器,即便向其传值,也不能进行变量赋值,你能够拿上述例子进行实验,不管你传递什么都是徒劳的,由于传递数据只能向上个暂停点进行传递,首个暂停点不存在上个暂停点。

生成器示例应用

了解生成器的基础知识后,咱们来一块儿作些有趣的练习:

斐波那契数列

首先咱们实现一个生成斐波那契数列的生成器函数,而后编写一个辅助函数用于进行控制输出指定位置的数,以下段代码所示:

function *fibonacciSequence() {  
    let x = 0, y = 1;  
    for(;;) {   
         yield y;    
        [x, y] = [y, x+y]; 
}}

function fibonacci(n) {  
    for(let f of fibonacciSequence()){    
        if (n-- <= 0) return f; }} console.log(fibonacci(20)) // => 10946复制代码

此函数只能返回指定位置的数值,若是返回指定位置的数列看起来会更加实用,以下段代码所示:

function *fibonacciSequence() {  
    let x = 0, y = 1;  
    for(;;) {   
         yield y;    
        [x, y] = [y, x+y]; 
}}

function* take(n, iterable) {  
    let it = iterable[Symbol.iterator](); 
      while(n-- > 0) {        
            let next = it.next();  
    if (next.done){
        return;
    }    
    else { 
        yield next.value
    }; 
}}

console.log([...take(5, fibonacciSequence())])
//[ 1, 1, 2, 3, 5 ]复制代码

多个生成器进行交错迭代

好比咱们要实现一个zip函数功能,相似Python的zip函数功能,将多个可迭代的对象合成一个新对象,合成对象的方法,就是循环依次从各个对象的位置进行取值合并,好比有两个数组a=[1,2,3],b=[4,5,6],合并后就是c=[1,4,2,5,3,6],如何用生成器进行实现呢?以下段代码所示:

function *oneDigitPrimes() { 
    yield 2;                   
    yield 3;               
    yield 5;                 
    yield 7;           
}
function *zip(...iterables) {  
    let iterators = iterables.map(i => i[Symbol.iterator]()); 
    let index = 0;  
    while(iterators.length > 0) { 
        if (index >= iterators.length)     
        index = 0;                       
        let item = iterators[index].next();   
        if (item.done) {                       
            iterators.splice(index, 1);      
            }
            else {
                yield item.value;                
                index++;
                }  
        }
}
console.log([...zip(oneDigitPrimes(),"ab",[0])]);
//[ 2, 'a', 0, 3, 'b', 5, 7 ]复制代码

从zip函数中咱们能够看出:

  • 首先经过Map函数将传入的可迭代对象进行实例化。
  • 而后循环可迭代对象,经过yield关键字调用next()方法进行返回输出。
  • 直到对应生成器数值消费完毕,移除对应的生成器(迭代器)对象。
  • 直到全部的生成器函数数值消费完,循环迭代的对象为空,函数中止执行。

经过向后追加的形式合并可迭代对象成一个新对象

function* oneDigitPrimes() { 
    yield 2;                   
    yield 3;               
    yield 5;                 
    yield 7;           
}
function* sequence(...iterables) {  
    for(let iterable of iterables) {   
         yield* iterable;  
        }}
console.log([...sequence("abc",oneDigitPrimes())])
//[ 'a', 'b', 'c', 2, 3, 5, 7 ]复制代码

使用生成器处理异步调用

假设有两个简单的异步函数

let getDataOne=(cb)=>{
    setTimeout(function () {
        cb('response data one');
    }, 1000);
};
let getDateTwo=(cb)=>{
    setTimeout(function () {
        cb('response data two')
    }, 1000)
}复制代码

将上述代码改为使用Generator,咱们使用next(value)的方法向生成器内部传值,代码以下:

let generator;
let getDataOne=()=>{
    setTimeout(function () {
        generator.next('response data one');
    }, 1000);
};
let getDateTwo=()=>{
    setTimeout(function () {
        generator.next('response data two')
    }, 1000)
}复制代码

接下来咱们来实现一个生成器函数main,调用上述方法,代码以下:

function *main() {
    let dataOne=yield getDataOne();
    let dataTwo=yield getDateTwo();
    console.log("data one",dataOne);
    console.log("data two",dataTwo);
}复制代码

怎么运行代码呢,其实很简单,以下所示:

generator=main();
generator.next();
//output
//data one response data one
//data two response data two复制代码

结果按照咱们的预期进行输出,并且main()函数的代码更加友好,和同步代码的感受是一致的,接下来是这样的:

  • 首先实例化生成器对象
  • 接下来咱们调用next()方法,启动生成器,生成器在第一行暂停,触发调用getDataOne()函数。
  • getDataOne()函数在1秒钟后,触发调用generator.next(‘response data one’),向生成器main内部变量dataOne传值,而后在yield getDateTwo()此处暂停,触发调用getDateTwo()。
  • getDateTwo()函数在1秒钟后,触发调用generator.next(‘response data two’),向生成器main内部变量dataTwo传值,而后运行下面console.log的内容,输出dataOne,dataTwo变量的值。

你是否是发现一个异步调用就和同步调用同样,但它是以异步的方式运行的。

一个真实的异步例子

例如咱们有一个需求,用NodeJs实现从论坛帖子列表数据中显示其中的一个帖子的信息及留言列表信息,代码以下:

DB/posts.json(帖子列表数据)

[
    {
        "id": "001",
        "title": "Greeting",
        "text": "Hello World",
        "author": "Jane Doe"
    },
    {
        "id": "002",
        "title": "JavaScript 101",
        "text": "The fundamentals of programming.",
        "author": "Alberta Williams"
    },
    {
        "id": "003",
        "title": "Async Programming",
        "text": "Callbacks, Promises and Async/Await.",
        "author": "Alberta Williams"
    }
]复制代码

DB/comments.json(评论列表)

[
    {
        "id": "phx732",
        "postId": "003",
        "text": "I don't get this callback stuff."
    },
    {
        "id": "avj9438",
        "postId": "003",
        "text": "This is really useful info."
    },
    {
        "id": "gnk368",
        "postId": "001",
        "text": "This is a test comment."
    }
]复制代码

用回调的方法实现代码以下 index.js

const fs = require('fs');
const path = require('path');
const postsUrl = path.join(__dirname, 'db/posts.json');
const commentsUrl = path.join(__dirname, 'db/comments.json');
//return the data from our file
function loadCollection(url, callback) {
    fs.readFile(url, 'utf8', function(error, data) {
        if (error) {
            console.log(error);
        } else {
            return callback(JSON.parse(data));
        }
    });
}
//return an object by id
function getRecord(collection, id, callback) {
    var collectobj=collection.find(function(element){
        return element.id == id;
    });
    callback(collectobj);
    return collectobj;
}
//return an array of comments for a post
function getCommentsByPost(comments, postId) {
    return comments.filter(function(comment){
        return comment.postId == postId;
    });
}
loadCollection(postsUrl, function(posts){
    loadCollection(commentsUrl, function(comments){
        getRecord(posts, "001", function(post){
            const postComments = getCommentsByPost(comments, post.id);
            console.log(post);
            console.log(postComments);
        });
    });
});复制代码

若是用生成器的方法如何实现呢?首先咱们改写loadCollection方法,代码以下:

let generator;
function loadCollection(url) {
    fs.readFile(url, 'utf8', function(error, data) {
        if (error) {
            generator.throw(error);
        } else {
            generator.next(JSON.parse(data));
        }
    });
}复制代码

接着咱们完成main generator 函数的实现,代码以下:

function *main() {
    let posts=yield loadCollection(postsUrl);
    let comments=yield loadCollection(commentsUrl);
    getRecord(posts, "001", function(post){
                const postComments = getCommentsByPost(comments, post.id);
                console.log(post);
                console.log(postComments);
            });
}复制代码

最后咱们进行调用

generator=main();
main().next();复制代码

将一个回调机制转换成一个生成器函数,看起来是否是很简洁易懂呢,咱们很轻松的建立了看似同步的异步代码。

小节

关于生成器(Generator)的介绍就到这里,它能够经过next方法暂停和恢复执行的函数。next方法还具有向生成器传递数据的功能,正是得益这个特色,才能帮助咱们解决异步代码的问题,让咱们建立了看似同步的异步代码,对于咱们来讲这个神器是否是特别的强大。

注:本文参考《javascript ES6 函数式编程入门经典》、《你不知道的javascript》、《JavaScript: The Definitive Guide, 7th Edition》

更多精彩内容,请微信关注”前端达人”公众号!

相关文章
相关标签/搜索