生成器虽然是ES6最具魔性的新特性,但也是最难懂得的一节,笔者写了大量的实例来具体化这种抽象的概念,能够让人一看就懂,目的是希望别人不要重复或者减少笔者学习生成器的痛苦经历。
在说具体的ES6生成器之前,我觉得我们应该先来看一个例子,因为它真的很晦涩,我们需要先增加一下兴趣会说话的猫
ES6生成器可以说是ES6新特性中最具魔力的特性,它可以极大的简化代码,并且贯穿了整个ES6的使用。它与我们以前学过的js特性都不同,它可以在函数的内部随时暂停。
那么我们从最简单的定义生成器说起,这可能会相对容易一些,以下是会说话的猫的生成器代码:
function* quips(name) { yield "hello " + name + "!"; yield "i hope you are enjoying the blog posts"; if (name.toLowerCase().startsWith("x")) { yield "hey, it's cool how your name starts with an X, " + name; } yield "see you later!"; }它看起来像一个函数,但是也有一些不同
- 它使用function*来声明,而不是function。
- 它使用yield来返回,而不是return。生成器函数可以yield多次而不会退出程序,在执行过程中遇到yield表达式立刻暂停,后续可恢复执行状态,而return直接退出程序。
那么这个生成器函数究竟是怎样执行的?
要理解内部运行机制,我们首先要清楚:生成器也是迭代器。为什么?因为所有的生成器都内建了.next()和Symbol.iterator方法 ,这个我们以后再说,现在只需要清楚这一点即可。
知道了生成器也是迭代器之后,现在我们用会说话的猫来分析生成器的执行过程。
var meow_iter = quips("Xingu"); //[object Generator] meow_iter.next(); //{value:"hello Xingu!",done:false} meow_iter.next(); //{value:"i hope you are enjoying the blog posts",done:false} meow_iter.next(); //{value:"hey, it's cool how your name starts with an X, Xingu",done:false} meow_iter.next(); //{value:"see you later!",done:false} meow_iter.next(); //{value:undefied,done:true}以上就是会说话的猫生成器的执行过程,看过ES6迭代器的同学可能基本上能看懂这段代码了。但是当你调用一个生成器的时候,它并非立即执行,而是在生成器函数第一句代码执行之前返回一个生成器对象(就是上面的meow_iter),这个时候函数冻结了。当你调用对象的.next()方法的时候,函数调用将其自身解冻并一直运行到下一个yield表达式,再次暂停。 当调用最后一个.next()的时候,返回的done为true,调用结束。这就是基本的生成器执行过程了。
要特别注意的一点是:生成器不是线程,它是按顺序执行的,不会发生并发。
有的同学就会问了,yield和return同时使用会发生什么,会不会因为眉来眼去最后发生一些故事呢?我们用实例来验证一下。
function* num(){ yield 1; yield 2; return 3; yield 4; } var itr = num(); console.log(itr.next()); console.log(itr.next()); console.log(itr.next()); console.log(itr.next()); console.log(itr.next()); // Object { value: 1, done: false } // Object { value: 2, done: false } // Object { value: 3, done: true } // Object { value: undefined, done: true } // Object { value: undefined, done: true }事实胜于雄辩,当我们在生成器中使用return的时候,那么return的值将会作为最后的返回值,而return后面的yield语句不会生效。
既然生成器是迭代器,那么我们用for-of来遍历一下带return的生成器又会怎样呢?
function* num(){ yield 1; yield 2; return 3; yield 4; } for(var i of num()){ console.log(i); } //1 //2可以看到for-of并不会遍历到return的值,也不会遍历到return之后yield的值。
之前我们yield都是处于正常语句中,那么如果处在循环中yield会发生什么情况?我想这个例子可以给我们一些参考。
function* num(){ console.log("start"); for(var i = 0; i < 3; i++){ yield i; } console.log("end"); } for(var i of num()){ console.log(i); } // start // 0 // 1 // 2 // end官方对与循环中yield是这样解释的:在循环中,每当生成器执行yields语句,生成器的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置)被移出堆栈。然而,生成器对象保留了对这个堆栈结构的引用(备份),所以稍后调用.next()可以重新激活堆栈结构并且继续执行。什么东西?好像很高深的样子。说了这么长一串,无非就是说循环中的yield和正常语句的yield在使用上没有区别。
生成器的基本使用就是以上的部分了,但是它远远不止这些特性,下面我们来探讨更加高级层次的特性。
yield*
以上我们讨论只是用yield返回单个值,如果要返回多个值该怎么办?在生成器中调用生成器该怎么办?yield* 表达式可以通过迭代器进行迭代生成所有的值(别忘了生成器也是迭代器)。
function* num(){ yield 1; yield 2; } function* num1(){ yield 3; yield* num(); yield 4 } for(var i of num1()){ console.log(i); } // 3 // 1 // 2 // 4
在生成器的.next()中传入参数会有什么诡异的表现呢?
这是一个不传入参数的例子
function* add(num){ var x = yield (num + 1); console.log(x); var y = yield (x + 1); return x + y; } var a = add(1); console.log(a.next()); console.log(a.next()); console.log(a.next()); // Object { value: 2, done: false } // undefined // Object { value: NaN, done: false } // Object { value: NaN, done: true }这是一个传入参数的例子
function* add(num){ var x = yield (num + 1); console.log(x); var y = yield (x + 1); return x + y; } var a = add(1); console.log(a.next()); console.log(a.next(2)); console.log(a.next(3)); // Object { value: 2, done: false } // 2 // Object { value: 3, done: false } // Object { value: 5, done: true }可见,我们在生成器中yield的返回值不能初始化左边的变量,实际上给.next()方法传参数, 那么这个参数将会作为上一次yield语句的返回值,如上我们传入2作为参数,那么上一个yield(num+1)就相当于返回了2,不然我们无法使用它的返回值,如例1。这样说还是有点难懂,一些复杂的问题往往因为一些简单的举例而变得容易理解。实际上这种给.next()传参的做法常常用来代替异步方式的Ajax请求,看完下面的例子你可能会恍然大悟。
"use strict"; function* main() { var result = yield request("http://www.filltext.com?rows=10&f={firstName}"); console.log(result); //do 别的ajax请求; } function request(url) { var r = new XMLHttpRequest(); r.open("GET", url, true); r.onreadystatechange = function () { if (r.readyState != 4 || r.status != 200) return; var data = JSON.parse(r.responseText); //数据成功返回以后, 代码就能够继续往下走了; it.next(data); }; r.send(); } var it = main(); it.next(); console.log("执行到这儿啦");it.next(data)将data作为上一次yield的返回值赋值给result,然后打印result,这种用同步的方式实现了和异步编程一样的效果,真是令人惊讶!
.return()和.throw()
要理解一些东西往往要从一些提问开始,现在我们看一下下面这段代码
function* producevalues() { setup(); try { // ... 生成一些值 } finally { cleanup(); } } for (var value of producevalues()) { work(value); }我们希望无论如何都在cleanup()中释放一些资源,但这里有一些情况:
- 我们没在 try 代码块中调用 work(value) ,如果它抛出异常怎么办?
-
或者假设 for-of 循环包含一条 break 语句或 return 语句。清理步骤又会如何执行呢?
任何真理都是靠实践出来的,下面我们写几个demo来测试一下
function* num() { try { for(var i = 0; i < 3; i++){ yield i; } } finally { console.log("finally run"); } } function work(value){ if(value != 1){ console.log(value); }else{ throw(new Error("error")); } } for(var value of num()) { work(value); } // 0 // finally run // Error: error循环中抛出异常,finally照常执行
function* num() { try { for(var i = 0; i < 3; i++){ yield i; } } finally { console.log("finally run"); } } function work(value){ console.log(value); } for(var value of num()) { if(value == 1){ break; } work(value); } // 0 // finally run循环中break或return,finally照常执行。
由此可见ES6中,清理时无论如何都会执行的。这是为什么?在迭代器章节的时候我们说过,迭代器接口支持一个可选的 .return() 方法,每当迭代在迭代器返回 {done:true} 之前退出都会自动调用这个方法。生成器支持这个方法, mygenerator.return() 会触发生成器执行任一 finally 代码块然后退出,就好像当前的生成暂停点已经被秘密转换为一条 return 语句一样。
这就是说,既可以隐式的调用return()方法(如中断循环break或抛出异常),也可以显示的调用。
以上都是隐式调用的,下面是一个显示调用举例。
function* num() { try { for(var i = 0; i < 3; i++){ yield i; } } finally { console.log("finally run"); } } function work(value){ console.log(value); } var a = num(); for(var value of a) { if(value == 1){ a.return(); } work(value); } // 0 // finally run // 1
生成器的.throw()方法也有要注意的地方,而且这个地方很怪.
如果执行到的yield在try语句块中,那么会被生成器内部的try捕获,否则被外部的try捕获,下面我们将实例验证一下:
yield在try语句块中
function* num(){ try{ yield 1 }catch(e){ console.log("内部捕获") } } var a = num(); a.next(); try{ a.throw("a"); }catch(e){ console.log("外部捕获") } //内部捕获
yield在try语句块外
function* num(){ try{ yield 1 }catch(e){ console.log("内部捕获") } yield 2 } var a = num(); a.next(); a.next(); try{ a.throw("a"); }catch(e){ console.log("外部捕获") } //外部捕获
最后补充一点关于生成器中的this以及它的原型
- 生成器中的this就是它的调用者。
- 生成器生成的Iterator,不但继承了Iterator的原型, 也继承了Generator的原型。
总结:通过以上的例子,相信大家已经掌握了生成器的内容,虽然它很难,但在人类的智慧面前,仍然不够看。