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 ...
随机推荐
-
xmind8
win10企业版安装 xmind.zip,打开应用程序报下面的错 解决办法是 先备份xmind8下的XMind.ini文件 Open up Xmind.ini and replace "Ap ...
-
IOS 解析crashlog
1.需要log.crash. dSYM和xcode自带的symbolicatecrash 放到一个文件夹下面 2.终端cd 到文件夹下面 运行命令 export DEVELOPER_DIR=/Appl ...
-
狗扑论坛 自动刷取VIP狗粮
狗扑论坛 自动刷取VIP狗粮 开始闲狗粮回复太慢就自己想了想去写一个,成功总是给我的哈哈. 自己花了一小时 时间慢慢学就慢慢写的 虽然代码简单 但是已经够自己用了 using System; usi ...
-
Git使用相关
Git使用相关 使用git这么久还是时不时碰到小问题,根本原因在于没有仔细研究和做笔记 Git修改remote地址 之前一直使用的ssh的地址,估计是没配置好,每次都需要输密码烦死了,今天看到个用ht ...
-
Android开发的七大环节
Android开发的七大环节 浏览:25 发布日期:2015/10/27 分类:职场感悟 一个完整的Android 开发流程主要包括策划.软件.交互.视觉.测试.运营维护这七大环节,其中的每一个环 ...
-
UIImagePickerController 操作图库
UIImagePickerController详解 转载自:http://blog.csdn.net/kingsley_cxz/article/details/9157093 1.UIImage ...
-
git SSh key多个key对应多个项目
必看 1. 本文不教你怎么生成key,主要解决多个项目对应多个SSH KEY的问题,在csdn code库上遇到的人估计很苦恼,为什么多个项目不能用一个key,为什么添加相同的key就会报重复 2. ...
-
四轴飞行器1.5 各种PID对比分析及选择
原创文章,欢迎转载,转载请注明出处 这篇文章主要介绍我对PID的理解,以及选择PID算法的过程. 一 PID的理解和学习过程 二 飞控的PID效果 先上个飞控PID的响应的视频:介绍在后面 地址:ht ...
-
【百度地图API】如何判断点击的是地图还是覆盖物?
原文:[百度地图API]如何判断点击的是地图还是覆盖物? 摘要:很多API爱好者问我,为什么我点击的是marker,而map也会响应该事件呢?怎样才能判断,我点击的是标注,还是地图呢?下面一起来看看. ...
-
【批处理】IF ERRORLEVER语句顺序注意
@echo off dir d:\dddddd if errorlevel 1 goto 1 if errorlevel 0 goto 0 rem 两行if语句不可交换位置,否则失败了也会显示成功. ...