生成器虽然是ES6最具魔性的新特性,但也是最难懂得的一节,笔者写了大量的实例来具体化这种抽象的概念,能够让人一看就懂,目的是希望别人不要重复或者减少笔者学习生成器的痛苦经历。

在说具体的ES6生成器之前,我觉得我们应该先来看一个例子,因为它真的很晦涩,我们需要先增加一下兴趣会说话的猫

ES6生成器可以说是ES6新特性中最具魔力的特性,它可以极大的简化代码,并且贯穿了整个ES6的使用。它与我们以前学过的js特性都不同,它可以在函数的内部随时暂停。

那么我们从最简单的定义生成器说起,这可能会相对容易一些,以下是会说话的猫的生成器代码:

  1. function* quips(name) {
  2. yield "hello " + name + "!";
  3. yield "i hope you are enjoying the blog posts";
  4. if (name.toLowerCase().startsWith("x")) {
  5. yield "hey, it's cool how your name starts with an X, " + name;
  6. }
  7. yield "see you later!";
  8. }

它看起来像一个函数,但是也有一些不同

  • 它使用function*来声明,而不是function。
  • 它使用yield来返回,而不是return。生成器函数可以yield多次而不会退出程序,在执行过程中遇到yield表达式立刻暂停,后续可恢复执行状态,而return直接退出程序。
那么这个生成器函数究竟是怎样执行的?
要理解内部运行机制,我们首先要清楚:生成器也是迭代器。为什么?因为所有的生成器都内建了.next()和Symbol.iterator方法 ,这个我们以后再说,现在只需要清楚这一点即可。
知道了生成器也是迭代器之后,现在我们用会说话的猫来分析生成器的执行过程。
  1. var meow_iter = quips("Xingu");
  2. //[object Generator]
  3. meow_iter.next();
  4. //{value:"hello Xingu!",done:false}
  5. meow_iter.next();
  6. //{value:"i hope you are enjoying the blog posts",done:false}
  7. meow_iter.next();
  8. //{value:"hey, it's cool how your name starts with an X, Xingu",done:false}
  9. meow_iter.next();
  10. //{value:"see you later!",done:false}
  11. meow_iter.next();
  12. //{value:undefied,done:true}

以上就是会说话的猫生成器的执行过程,看过ES6迭代器的同学可能基本上能看懂这段代码了。但是当你调用一个生成器的时候,它并非立即执行,而是在生成器函数第一句代码执行之前返回一个生成器对象(就是上面的meow_iter),这个时候函数冻结了。当你调用对象的.next()方法的时候,函数调用将其自身解冻并一直运行到下一个yield表达式,再次暂停。 当调用最后一个.next()的时候,返回的done为true,调用结束。这就是基本的生成器执行过程了。


要特别注意的一点是:生成器不是线程,它是按顺序执行的,不会发生并发。

有的同学就会问了,yield和return同时使用会发生什么,会不会因为眉来眼去最后发生一些故事呢?我们用实例来验证一下。
  1. function* num(){
  2. yield 1;
  3. yield 2;
  4. return 3;
  5. yield 4;
  6. }
  7. var itr = num();
  8. console.log(itr.next());
  9. console.log(itr.next());
  10. console.log(itr.next());
  11. console.log(itr.next());
  12. console.log(itr.next());
  13. // Object { value: 1, done: false }
  14. // Object { value: 2, done: false }
  15. // Object { value: 3, done: true }
  16. // Object { value: undefined, done: true }
  17. // Object { value: undefined, done: true }

事实胜于雄辩,当我们在生成器中使用return的时候,那么return的值将会作为最后的返回值,而return后面的yield语句不会生效。


既然生成器是迭代器,那么我们用for-of来遍历一下带return的生成器又会怎样呢?
  1. function* num(){
  2. yield 1;
  3. yield 2;
  4. return 3;
  5. yield 4;
  6. }
  7. for(var i of num()){
  8. console.log(i);
  9. }
  10. //1
  11. //2

可以看到for-of并不会遍历到return的值,也不会遍历到return之后yield的值。


之前我们yield都是处于正常语句中,那么如果处在循环中yield会发生什么情况?我想这个例子可以给我们一些参考。
  1. function* num(){
  2. console.log("start");
  3. for(var i = 0; i < 3; i++){
  4. yield i;
  5. }
  6. console.log("end");
  7. }
  8. for(var i of num()){
  9. console.log(i);
  10. }
  11. // start
  12. // 0
  13. // 1
  14. // 2
  15. // end

官方对与循环中yield是这样解释的:在循环中,每当生成器执行yields语句,生成器的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置)被移出堆栈。然而,生成器对象保留了对这个堆栈结构的引用(备份),所以稍后调用.next()可以重新激活堆栈结构并且继续执行。什么东西?好像很高深的样子。说了这么长一串,无非就是说循环中的yield和正常语句的yield在使用上没有区别。


生成器的基本使用就是以上的部分了,但是它远远不止这些特性,下面我们来探讨更加高级层次的特性。

yield*
以上我们讨论只是用yield返回单个值,如果要返回多个值该怎么办?在生成器中调用生成器该怎么办?yield* 表达式可以通过迭代器进行迭代生成所有的值(别忘了生成器也是迭代器)。
  1. function* num(){
  2. yield 1;
  3. yield 2;
  4. }
  5. function* num1(){
  6. yield 3;
  7. yield* num();
  8. yield 4
  9. }
  10. for(var i of num1()){
  11. console.log(i);
  12. }
  13. // 3
  14. // 1
  15. // 2
  16. // 4

.next()传参

在生成器的.next()中传入参数会有什么诡异的表现呢?
这是一个不传入参数的例子
  1. function* add(num){
  2. var x = yield (num + 1);
  3. console.log(x);
  4. var y = yield (x + 1);
  5. return x + y;
  6. }
  7. var a = add(1);
  8. console.log(a.next());
  9. console.log(a.next());
  10. console.log(a.next());
  11. // Object { value: 2, done: false }
  12. // undefined
  13. // Object { value: NaN, done: false }
  14. // Object { value: NaN, done: true }

这是一个传入参数的例子

  1. function* add(num){
  2. var x = yield (num + 1);
  3. console.log(x);
  4. var y = yield (x + 1);
  5. return x + y;
  6. }
  7. var a = add(1);
  8. console.log(a.next());
  9. console.log(a.next(2));
  10. console.log(a.next(3));
  11. // Object { value: 2, done: false }
  12. // 2
  13. // Object { value: 3, done: false }
  14. // Object { value: 5, done: true }

可见,我们在生成器中yield的返回值不能初始化左边的变量,实际上给.next()方法传参数, 那么这个参数将会作为上一次yield语句的返回值,如上我们传入2作为参数,那么上一个yield(num+1)就相当于返回了2,不然我们无法使用它的返回值,如例1。这样说还是有点难懂,一些复杂的问题往往因为一些简单的举例而变得容易理解。实际上这种给.next()传参的做法常常用来代替异步方式的Ajax请求,看完下面的例子你可能会恍然大悟。

  1. "use strict";
  2. function* main() {
  3. var result = yield request("http://www.filltext.com?rows=10&f={firstName}");
  4. console.log(result);
  5. //do 别的ajax请求;
  6. }
  7.  
  8. function request(url) {
  9. var r = new XMLHttpRequest();
  10. r.open("GET", url, true);
  11. r.onreadystatechange = function () {
  12. if (r.readyState != 4 || r.status != 200) return;
  13. var data = JSON.parse(r.responseText);
  14. //数据成功返回以后, 代码就能够继续往下走了;
  15. it.next(data);
  16. };
  17. r.send();
  18. }
  19.  
  20. var it = main();
  21. it.next();
  22. console.log("执行到这儿啦");

it.next(data)将data作为上一次yield的返回值赋值给result,然后打印result,这种用同步的方式实现了和异步编程一样的效果,真是令人惊讶!


.return()和.throw()
要理解一些东西往往要从一些提问开始,现在我们看一下下面这段代码
  1. function* producevalues() {
  2. setup();
  3. try {
  4. // ... 生成一些值
  5. } finally {
  6. cleanup();
  7. }
  8. }
  9. for (var value of producevalues()) {
  10. work(value);
  11. }

我们希望无论如何都在cleanup()中释放一些资源,但这里有一些情况:

  • 我们没在 try 代码块中调用 work(value) ,如果它抛出异常怎么办?
  • 或者假设 for-of 循环包含一条 break 语句或 return 语句。清理步骤又会如何执行呢?
任何真理都是靠实践出来的,下面我们写几个demo来测试一下
  1. function* num() {
  2. try {
  3. for(var i = 0; i < 3; i++){
  4. yield i;
  5. }
  6. } finally {
  7. console.log("finally run");
  8. }
  9. }
  10. function work(value){
  11. if(value != 1){
  12. console.log(value);
  13. }else{
  14. throw(new Error("error"));
  15. }
  16. }
  17. for(var value of num()) {
  18. work(value);
  19. }
  20. // 0
  21. // finally run
  22. // Error: error

循环中抛出异常,finally照常执行

  1. function* num() {
  2. try {
  3. for(var i = 0; i < 3; i++){
  4. yield i;
  5. }
  6. } finally {
  7. console.log("finally run");
  8. }
  9. }
  10. function work(value){
  11. console.log(value);
  12. }
  13. for(var value of num()) {
  14. if(value == 1){
  15. break;
  16. }
  17. work(value);
  18. }
  19. // 0
  20. // finally run

循环中break或return,finally照常执行。


由此可见ES6中,清理时无论如何都会执行的。这是为什么?在迭代器章节的时候我们说过,迭代器接口支持一个可选的 .return() 方法,每当迭代在迭代器返回 {done:true} 之前退出都会自动调用这个方法。生成器支持这个方法, mygenerator.return() 会触发生成器执行任一 finally 代码块然后退出,就好像当前的生成暂停点已经被秘密转换为一条 return 语句一样。 
这就是说,既可以隐式的调用return()方法(如中断循环break或抛出异常),也可以显示的调用。

以上都是隐式调用的,下面是一个显示调用举例。
  1. function* num() {
  2. try {
  3. for(var i = 0; i < 3; i++){
  4. yield i;
  5. }
  6. } finally {
  7. console.log("finally run");
  8. }
  9. }
  10. function work(value){
  11. console.log(value);
  12. }
  13. var a = num();
  14. for(var value of a) {
  15. if(value == 1){
  16. a.return();
  17. }
  18. work(value);
  19. }
  20. // 0
  21. // finally run
  22. // 1

下面我们介绍.throw()方法

生成器的.throw()方法也有要注意的地方,而且这个地方很怪.
如果执行到的yield在try语句块中,那么会被生成器内部的try捕获,否则被外部的try捕获,下面我们将实例验证一下:

yield在try语句块中
  1. function* num(){
  2. try{
  3. yield 1
  4. }catch(e){
  5. console.log("内部捕获")
  6. }
  7. }
  8. var a = num();
  9. a.next();
  10. try{
  11. a.throw("a");
  12. }catch(e){
  13. console.log("外部捕获")
  14. }
  15. //内部捕获

yield在try语句块外
  1. function* num(){
  2. try{
  3. yield 1
  4. }catch(e){
  5. console.log("内部捕获")
  6. }
  7. yield 2
  8. }
  9. var a = num();
  10. a.next();
  11. a.next();
  12. try{
  13. a.throw("a");
  14. }catch(e){
  15. console.log("外部捕获")
  16. }
  17. //外部捕获

最后补充一点关于生成器中的this以及它的原型

  • 生成器中的this就是它的调用者。
  • 生成器生成的Iterator,不但继承了Iterator的原型, 也继承了Generator的原型。

总结:通过以上的例子,相信大家已经掌握了生成器的内容,虽然它很难,但在人类的智慧面前,仍然不够看。










轻松学会ES6新特性之生成器的更多相关文章

  1. ES6新特性之生成器函数 (generator function): function*

    一.什么是生成器函数(generator function)? 生成器函数是ES6的新特性之一,它是一个在执行时能中途暂时退出,后面重新调用又能重新进入继续执行的一种函数. 并且在函数内定义的变量的所 ...

  2. ES6新特性三: Generator(生成器)函数详解

    本文实例讲述了ES6新特性三: Generator(生成器)函数.分享给大家供大家参考,具体如下: 1. 简介 ① 理解:可以把它理解成一个函数的内部状态的遍历器,每调用一次,函数的内部状态发生一次改 ...

  3. ES6新特性概览

    本文基于lukehoban/es6features ,同时参考了大量博客资料,具体见文末引用. ES6(ECMAScript 6)是即将到来的新版本JavaScript语言的标准,代号harmony( ...

  4. Atitit js版本es5 es6新特性

    Atitit js版本es5 es6新特性 Es5( es5 其实就是adobe action script的标准化)1 es6新特性1 Es5( es5 其实就是adobe action scrip ...

  5. ES6新特性简介

    ES6新特性简介 环境安装 npm install -g babel npm install -g babel-node //提供基于node的REPL环境 //创建 .babelrc 文件 {&qu ...

  6. 必须掌握的ES6新特性

    ES6(ECMAScript2015)的出现,让前端开发者收到一份惊喜,它简洁的新语法.强大的新特性,带给我们更便捷和顺畅的编码体验,赞! 以下是ES6排名前十的最佳特性列表(排名不分先后): 1.D ...

  7. 你不知道的JavaScript--Item24 ES6新特性概览

    ES6新特性概览 本文基于lukehoban/es6features ,同时参考了大量博客资料,具体见文末引用. ES6(ECMAScript 6)是即将到来的新版本JavaScript语言的标准,代 ...

  8. 34.js----JS 开发者必须知道的十个 ES6 新特性

    JS 开发者必须知道的十个 ES6 新特性 这是为忙碌的开发者准备的ES6中最棒的十个特性(无特定顺序): 默认参数 模版表达式 多行字符串 拆包表达式 改进的对象表达式 箭头函数 =&> ...

  9. ES6新特性概览1

    本文基于lukehoban/es6features ,同时参考了大量博客资料,具体见文末引用. ES6(ECMAScript 6)是即将到来的新版本JavaScript语言的标准,代号harmony( ...

随机推荐

  1. 14.LINUX-platform机制实现驱动层分离(详解)

    版权声明:本文为博主原创文章,未经博主允许不得转载. 本节目标:        学习platform机制,如何实现驱动层分离 1.先来看看我们之前分析输入子系统的分层概念,如下图所示: 如上图所示,分 ...

  2. 异常 SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]

    使用Spring 的JDBCtemplate 调用数据库的时候 出现了如下的问题 SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-S ...

  3. Servlet学习应该注意的几点

    一.Servlet生命周期(即运行过程) (1)初始阶段,调用init()方法 (2)响应客户请求阶段,调用service()方法.由service()方法根据提交方式不同执行doGet()或doPo ...

  4. Struts2的核心运行流程,原理图解

    感觉很有必要制定一个计划,这样盲目的想到哪里写到哪里,不符合我大程序员的思维逻辑呀~~~嗯...那就从基本的开始吧,循循渐进,今天想到的先写上,不能浪费了,哈哈哈................... ...

  5. macbook 263企业邮箱设置

    第一步:打开邮箱,点击添加账号,选择其他 第二步:填写完整的电子邮件地址和密码 第三步:填写收件服务器(popcom.263xmail.com),发件服务器(smtpcom.263xmail.com)

  6. ThinkPHP中foreach和volist的区别

    1.foreach标签foreach标签用于循环输出:foreach(name,item,key)name(必须):要输出的数据模板变量item(必须):循环单原变量key(可选):循环的key变量, ...

  7. swiper拖拽之后不自动滑动问题

    //swiper轮播图 var mySwiper = new Swiper('.swiper-container',{ initialSlide :0, autoplay : 3000, direct ...

  8. fitnesse - 一个简单的例子(slim)

    fitnesse - 一个简单的例子(slim) 2017-09-30 目录1 编写测试代码(Fixture code)2 编写wiki page并运行  2.1 新建wikiPage  2.2 运行 ...

  9. 浅谈Java抽象类

    什么是抽象类?这名字听着就挺抽象的,第一次听到这个名字还真有可能被唬住.但是,就像老人家所说的,一切反动派都是纸老虎,一切有着装x名字的概念也是纸老虎.好吧,我们已经从战略上做到了藐视它,现在就要战术 ...

  10. Elasticsearch-sql 用SQL查询Elasticsearch

    Elasticsearch的查询语言(DSL)真是不好写,偏偏查询的功能千奇百怪,filter/query/match/agg/geo各种各样,不管你是通过封装JSON还是通过python/java的 ...