【ES6基础】生成器(Generator)
栏目: JavaScript · 发布时间: 5年前
内容简介:在这篇文章里今天笔者将从以下几个方面进行介绍生成器(Generator):本篇文章阅读时间预计
在这篇文章里 《【ES6基础】迭代器(iterator)》 ,笔者介绍了迭代器及相关实例,我们要实现一个迭代器要写不少的代码。幸运的是,ES6引入了一个新的函数类型——生成器函数(Generator function),让我们能够更轻松更便捷的实现迭代器的相关功能。
今天笔者将从以下几个方面进行介绍生成器(Generator):
- 什么是生成器
- 生成器的基本语法
- yield关键字
- 生成器函数的类型检测
- yield*委托
- return(value)方法
- throw(exception)方法
- 向生成器传递数据
- 生成器示例应用
本篇文章阅读时间预计 15 分钟
什么是生成器?
生成器第一次出现在CLU语言中。CLU语言是美国麻省理工大学的Barbara Liskov教授和她的学生们在1974年至1975年间所设计和开发出来的。Python、C#和 Ruby 等语言都受到其影响,实现了生成器的特性,生成器在CLU和C#语言中被称为迭代器(iterator),Ruby语言中称为枚举器(Enumerator)。
生成器的主要功能是:通过一段程序,持续迭代或枚举出符合某个公式或算法的有序数列中的元素。这个程序便是用于实现这个公司或算法的,而不需要将目标数列完整写出。
在ES6定义的生成器函数有别于普通的函数,生成器可以在执行当中暂停自身,可以立即恢复执行也可以过一段时间之后恢复执行。最大的区别就是它并不像普通函数那样保证运行到完毕。还有一点就是,在执行当中每次暂停或恢复循环都提供了一个双向信息传递的机会,生成器可以返回一个值,恢复它的控制代码也可以返回一个值。
生成器的基本语法
与普通函数语法的差别,在function关键字和函数名直接有个*号,这个*作为生成器函数的主要标识符,如下所示:
function *it(){}复制代码
*号的位置没有严格规定,只要在中间就行,你可以这么写:
function *it(){ } function* it(){ } function * it(){ } function*it(){ }复制代码
笔者觉得*靠近函数名——function *it(){ },看着更为清晰,选择哪种书写方式完全凭个人喜好。
调用生成器也十分简单,就和调用普通函数一样,比如:
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); //4 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,并在此行暂停。
- 运行到最后一行,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》、《The Definitive Guide, 7th Edition》
【ES6基础】解构赋值(destructuring assignment)
【css基础】如何理解transform的matrix()用法
更多精彩内容,请微信关注”前端达人”公众号!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。