ES6 Generators基本概念
ES6 Generators系列:
在JavaScript ES6提供的诸多令人兴奋的新特性中,有一个新函数类型,叫generator。名字听起来很怪(我们姑且将它称之为生成器函数),而且行为更加让人觉得怪异。本文旨在解释generator函数的一些基本知识,用来说明它是如何工作的,并帮助你了解为什么它会让未来的JS变得如此强大。
运行-完成(Run-To-Completion)
首先我们要讨论的是generator函数和普通函数在运行方式上有什么区别。
不论你是否已经意识到了,对于函数而言,你总是会假定一个原则:一旦函数开始运行,它就会在其它JS代码运行之前运行到结束。这句话怎么理解呢?看下面的代码:
setTimeout(function(){
console.log("Hello World");
},1);
function foo() {
// NOTE: don't ever do crazy long-running loops like this
for (var i=0; i<=1E10; i++) {
console.log(i);
}
}
foo();
// 0..1E10
// "Hello World"
这里的for循环需要一个比较长的时间来执行完,显然超过1毫秒。在foo()函数运行过程中,上面的setTimeout函数不会被运行直到foo()函数运行结束。
那如果事情不是这样的会怎么样?如果foo()函数的运行会被setTimeout打断呢?是不是我们的程序将会变得不稳定?
在多线程运行的程序中,这的确会给你带来噩梦,好在JavaScript是单线程运行的(同一时间只有一条命令或函数会被运行),因此这一点你不必担心。
注意,Web开发允许JS程序的一部分在一个独立的线程里运行,该线程可以与JS主线程并行运行。但这并不意味着我们可以在JS程序中引入多线程操作,因为在多线程操作中两个独立的线程之间是可以通过异步事件相互通信的,它们彼此之间通过事件轮询机制(event-loop)一次一个地来运行。
运行-停止-运行(Run-Stop-Run)
ES6的generator函数允许在运行的过程中暂停一次或多次,随后再恢复运行。暂停的过程中允许其它的代码执行。
如果你曾经读过有关并发或者线程编程方面的文章,你也许见到过"cooperative"(协作)一词,它说明了一个进程(这里可以将它理解为一个function)本身可以选择何时被中断以便与其它代码进行协作。这个概念与"preemptive"(抢占式。进程调度的一种方式。当前进程在运行过程中,如果有重要或紧迫的进程到达(其状态必须为就绪),则该进程将被迫放弃处理机,系统将处理机立刻分配给新到达的进程。)正好相反,它表明了一个进程或function可以被其自身的意愿打断。
在ES6中,generator函数使用的都是cooperative类型的并发方式。在generator函数体内,通过使用新的yield关键字从内部将函数的运行打断。除了generator函数内部的yield关键字,你不可能从任何地方(包括函数外部)中断函数的运行。
不过,一旦generator函数被中断,它不可能自行恢复运行,除非通过外部的控制来重新启动这个generator函数。稍后我会介绍如何实现这一点。
基本上,按照需要,一个generator函数在运行中可以被停止和重新启动多次。事实上,你完全可以指定一个无限循环的generator函数(就像while(true){...}语句一样),它永远也不会被执行完。不过在一个正常的JS程序中,我们通常不会这样做,除非代码写错了。Generator函数足够理性,有时候它恰恰就是你想要的!
而更重要的是,这种停止和启动不仅仅控制着generator函数的执行,它还允许信息的双向传递。普通函数在开始的时候获取参数,在结束的时候return一个值,而generator函数可以在每次yield的时候返回值,并且在下一次重新启动的时候再传入值。
语法
是时候介绍一下generator函数的语法了:
function *foo() {
// ..
}
注意这里的*了吗?这是一个新引入的运算符,对于学习C语言系的同学而言,可能会想到函数指针。不过这里千万不要把它和指针的概念混淆了,*运算符在这里只是用来标识generator函数的类型。
你可能在其它的文章或文档中看到这种写法function* foo(){},而本文中我们使用这种写法function *foo(){}(区别仅仅是*的位置)。这两种写法都是正确的,不过我们推荐使用后者。
我们来看看generator函数的内容。Generator函数在大多数方面就是普通的JS函数,因此我们需要学习的新语法不会很多。
在generator函数体内部主要是yield关键字的应用,前面我们已经提到过它。注意这里的yield ___被称之为yield表达式而不是语句,这是因为当我们重新启动generator函数时,我们会传入一个值,而不管这个值是什么,都会作为yield ___表达式计算的结果。
一个例子:
function *foo() {
var x = 1 + (yield "foo");
console.log(x);
}
这里的yield "foo"表达式会在generator函数暂停时返回字符串"foo",当下一次generator函数重新启动时,不管传入的值是什么,都会作为yield表达式计算的结果。这里会将表达式1 + 传入值的结果赋值给变量x。
从这个意义上来说,generator函数具有双向通信的功能。Generator函数暂停的时候返回了字符串"foo",稍后(可能是立即,也可能是从现在开始一段很长的时间)重新启动的时候它会请求一个新值并将最终计算的结果返回。这里的yield关键字起到了请求新值的作用。
在任何表达式中,你可以只用yield关键字而不带其它内容,此时yield返回的值是undefined。看下面的例子:
// 注意,这里的函数foo(..)不是一个generator函数!!
function foo(x) {
console.log("x: " + x);
} function *bar() {
yield; // 暂停执行,返回值是undefined
foo( yield ); // 暂停执行,稍后将获取到的值作为函数foo(..)的参数传入
}
Generator遍历器
“Generator遍历器”!乍一看,好像很难懂!
遍历器是一种特殊的行为,实际上是一种设计模式,我们通过调用next()方法来遍历一组有序的值。想象一下,例如使用遍历器对数组[1,2,3,4,5]进行遍历。第一次调用next()方法返回1,第二次调用next()方法返回2,以此类推。当数组中的所有值都返回后,调用next()方法将返回null或false或其它可能的值用来表示数组中的所有元素都已遍历完毕。
我们唯一可以从外部控制generator函数的方式就是构造和通过遍历器进行遍历。这听起来好像有点复杂,考虑下面这个简单的例子:
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
为了遍历generator函数*foo(),首先我们需要构造一个遍历器。怎么做?很简单!
var it = foo();
事实上,通过普通的方式调用一个generator函数并不会真正地执行它。
这有点让人难以理解。你可能在想,为什么不是var it = new foo(). 背后的原理已经超出了我们的范围,这里我们不展开讨论。
然后,我们通过下面的方法对generator函数进行遍历:
var message = it.next();
这会执行yield 1表达式并返回值1,但不仅限于此。
console.log(message); // { value:1, done:false }
事实上每次调用next()方法都会返回一个object对象,其中的value属性就是yield表达式返回的值,而属性done是一个boolean类型,用来表示对generator函数的遍历是否已经结束。
继续看剩余的几个遍历:
console.log( it.next() ); // { value:2, done:false }
console.log( it.next() ); // { value:3, done:false }
console.log( it.next() ); // { value:4, done:false }
console.log( it.next() ); // { value:5, done:false }
有趣的是,当value的值是5时done仍然是false。这是因为从技术上来说,generator函数还没有执行完,我们必须再调用一次next()方法,如果此时传入一个值(如果未传入值,则默认为undefined),它会被设置为yield 5表达式计算的结果,然后generator函数才算执行完毕。
因此:
console.log( it.next() ); // { value:undefined, done:true }
所以,最终的结果是我们完成了generator函数的调用,但是最后一次的遍历并没有返回任何值,这是因为所有的yield表达式都已经被执行完了。
你或许在想,我们可以在generator函数中使用return语句吗?如果可以的话,那value属性的值会被返回吗?
答案是肯定的:
function *foo() {
yield 1;
return 2;
}
var it = foo();
console.log( it.next() ); // { value:1, done:false }
console.log( it.next() ); // { value:2, done:true }
但是:
依赖generator函数中return语句返回的值并不值得提倡,因为当使用for..of循环(下面会介绍)来遍历generator函数时,最后的return语句可能会导致异常。
我们来完整地看一下在遍历generator函数时信息是如何被传入和传出的:
function *foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var it = foo( 5 );
// 注意这里在调用next()方法时没有传入任何值
console.log( it.next() ); // { value:6, done:false }
console.log( it.next( 12 ) ); // { value:8, done:false }
console.log( it.next( 13 ) ); // { value:42, done:true }
你可以看到我们在构造generator函数遍历器的时候仍然可以传递参数,这和普通的函数调用一样,通过语句foo(5),我们将参数x的值设置为5。
第一次调用next()方法时,没有传入任何值。为什么呢?因为此时没有yield表达式来接收我们传入的值。
如果在第一次调用next()方法时传入一个值,也不会有任何影响,该值会被抛弃掉。按照ES6标准的规定,此时generator函数会直接忽略掉该值(注意:在撰写本文时,Chrome和FireFox浏览器都能很好地符合该规定,但其它浏览器可能并不完全符合,而且可能会抛出异常)。
表达式yield(x + 1)的返回值是6,然后第二个next(12)将12作为参数传入,用来代替表达式yield(x + 1),因此变量y的值就是12 × 2,即24。随后的yield(y / 3)(即yield(24 / 3))返回值8。然后第三个next(13)将13作为参数传入,用来代替表达式yield(y / 3),所以变量z的值是13。
最后,语句return (x + y + z)即return (5 + 24 + 13),所以最终的返回值是42。
多重温几次上面的代码,开始的时候你会觉得很难懂,只要理解了generator函数执行的过程,掌握起来并不难。
for..of循环
ES6还从语法层面上对遍历器提供了直接的支持,即for..of循环。看下面的例子:
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (var v of foo()) {
console.log( v );
}
// 1 2 3 4 5
console.log( v ); // 仍然是5,而不是6
正如你所看到的,由foo()创建的遍历器被for..of循环自动捕获,然后自动进行遍历,每遍历一次就返回一个值,直到属性done的值为true。只要属性done的值为false,它就会自动提取value属性的值并将其传递给迭代变量(本例中为变量v)。一旦属性done的值为true,循环遍历就停止(而且不会包含函数的返回值,如果有的话。所以此处的return 6不包括在for..of循环中)。
如上所述,可以看到for..of循环忽略并抛弃了返回值6,这是因为此处没有对应的next()方法被调用,for..of循环不支持将值传递给generator函数迭代的情况,如在for..of循环中使用next(v)。事实上,在使用for..of循环时不需要使用next方法。
总结
以上就是generator函数的基本概念。如果你仍然觉得有点难以理解,也不用太担心,任何人刚开始接触generator函数时都会有这种感觉!
你应该会很自然地想到generator函数能在自己的代码中起到什么样的作用,尽管我们会在很多地方用到它。我们刚刚只是接触到了一些皮毛,还有很多需要了解的,所以我们必须深入研究,才能发现它是如此的强大。
尝试在Chrome nightly/canary或FireFox nightly或node 0.11+(使用--harmony参数)环境中运行本文的示例代码,并思考下面的问题:
- 如何处理异常?
- 在一个generator函数中可以调用另一个generator函数吗?
- 如何在generator函数中进行异步编程?
接下来的文章会解答上述问题,并继续深入探讨有关ES6 generator函数的内容,敬请关注!
ES6 Generators基本概念的更多相关文章
- ES6 Generators并发
ES6 Generators系列: ES6 Generators基本概念 深入研究ES6 Generators ES6 Generators的异步应用 ES6 Generators并发 如果你已经读过 ...
- ES6 Generators的异步应用
ES6 Generators系列: ES6 Generators基本概念 深入研究ES6 Generators ES6 Generators的异步应用 ES6 Generators并发 通过前面两篇文 ...
- 深入研究ES6 Generators
ES6 Generators系列: ES6 Generators基本概念 深入研究ES6 Generators ES6 Generators的异步应用 ES6 Generators并发 如果你还不知道 ...
- ES6 generators in depth 一(译)
今天在学习redux-saga时,外部链接推荐了这篇文章ES6 generators in depth,所以翻译的同时也可以加深一下对Generator的理解. 这里对原文一些只能在高版本现代浏览器使 ...
- [ES6] Generators
Example 1: function *topicList(){ yield "ES2015"; yield "Semi-colons: good or bad?&qu ...
- ES6深度解析3:Generators
介绍ES6 Generators 什么是Generators(生成器函数)?让我们先来看看一个例子. function* quips(name) { yield "hello " ...
- 【转向Javascript系列】深入理解Generators
随着Javascript语言的发展,ES6规范为我们带来了许多新的内容,其中生成器Generators是一项重要的特性.利用这一特性,我们可以简化迭代器的创建,更加令人兴奋的,是Generators允 ...
- 30分钟ES6从陌生到熟悉
前言 ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了.它的目标,是使得 JavaScript 语言可以用来编写复杂的 ...
- 【翻译】ES6生成器简介
原文地址:http://davidwalsh.name/es6-generators ES6生成器全部文章: The Basics Of ES6 Generators Diving Deeper Wi ...
随机推荐
- C# httpclient获取cookies实现模拟web登录
目前在公司做一款平台化的产品,我主要负责PC端上的开发,在产品推荐过程中为了节省开发时间很多功能模块没来得及做原生,用CEF嵌入了很多带功能web页面,与客户端进行交互从而实现功能. 在二期开发中,产 ...
- 小程序使用wx.chooseAddress获取用户手机号码,微信chooseAddress接口获取用户收货信息
通常用户在商城购买产品后,需要填写他的收货信息,方便我们发货,但是在手机上写字非常不方便,一个客户的收货信息包括:姓名,地址和手机号码这些内容全部填写的话,至少要写20个字. 地址 所以有些客户在手机 ...
- 查看主机DNSserver
一.Nslookup(name server lookup)( 域名查询):是一个用于查询 Internet 域名信息或诊断DNS server问题的工具.nslookup能够指定查询的类型,能够查到 ...
- logstash 向elasticsearch写入数据,怎样指定多个数据template
之前在配置从logstash写数据到elasticsearch时,指定单个数据模板没有问题.可是在配置多个数据模板时候,总是不成功,后来找了非常多资料,最终找到解决的方法,就是要多加一个配置项: te ...
- File attachment or query results size exceeds allowable value of 1000000 bytes.
今天早晨,收到了作业执行失败的邮件(前几天还能正常执行该作业.不知为何今天出错) 邮件显示,作业的第三个步骤报错. step3内容: msdb.dbo.sp_send_dbmail @prof ...
- xml解析(4)
本节要点: DOM解析方式 SAX解析方式 DOM4J对XML的解析 XML用于将数据组织起来,形成文档用于存储和传输: 更多时候我们需要的是将xml中的数据解析出来,甚至是在程序中动态生成xml. ...
- 将CSS放头部,JS放底部,可以提高页面的性能的原因
css不阻止dom的解析 js阻止dom的解析 css js都会阻止dom的渲染 原因: js有可能影响dom的解析,比如在js里面新增dom等这些操作 css不能影响dom的解析 而 dom的渲染 ...
- Asp.net MVC 简单实现生成Excel并下载
由于项目上的需求,需要导出指定条件的Excel文件.经过一翻折腾终于实现了. 现在把代码贴出来分享 (直接把我们项目里面的一部份辅助类的代码分享一下) 我们项目使用的是Asp.Net MVC4.0模式 ...
- 3D Game Programming withDX11 学习笔记(一) 数学知识总结
在图形学中,数学是不可或缺的一部分,所以本书最开始的部分就是数学知识的复习.在图形学中,最常用的是矢量和矩阵,所以我根据前面三个章节的数学知识,总结一下数学知识. 一.矢量 数学中的矢量,拥有方向和长 ...
- [原创]mysql的zip包如何在windows下安装
今天在尝试zipkin的链路数据写入mysql,本机恰好没有按照mysql.找到一个很久前谁发的mysql-5.6.19-winx64.zip,版本不新?别挑剔啦,只是本机测试,能用就好哈哈.. 解压 ...