ECMAScript 6 笔记(22)- Generator 函数的语法(2)

原创 乘风逐月 随笔 ES6 40阅读 22 天前 举报

一、Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后 Generator 函数体内捕获。

上例中,遍历器对象 i 连续抛出两个错误。第一个错误被 Generator 函数体内的 catch 语句捕获。i 第二次抛出错误,由于 Generator 函数内部的 catch 语句已经执行过了,不会再捕获到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的 catch 语句捕获。
throw 方法可以接受一个参数,该参数会被 catch 语句接收,建议抛出 Error 对象的实例。

注意:
不要混淆遍历器对象的 throw 方法和全局的 throw 命令。上例中,错误是遍历器的 throw 方法抛出的,而不是 throw 命令抛出的。 throw 命令抛出的错误只能被函数体外的 catch 语句捕获。
上例只捕获了 a,是因为函数体外的 catch 语句块捕获了抛出的 a 错误以后,就不会再继续 try 代码块里面 剩余的语句了。

关于 throw 方法抛出的错误捕获
(1) 如果 Generator 函数内部有部署 try...catch 代码块,那么 throw 方法抛出的错误,将被内部的 try...catch 代码块捕获。
(2) 如果 Generator 函数内部没有部署 try...catch 代码块,那么 throw 方法抛出的错误,将被外部的 try...catch 代码块捕获。
(3) 如果 Generator 函数内部和外部,都没有部署 try...catch 代码块,那么程序将会报错,直接中断执行。
(4) throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next 方法。

上例中,next 方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。这是因为第一次执行 next 方法,等同于启动执行 Generator 函数内部的代码,否则 Generator 函数还没有开始执行,这时 throw 方法抛出错误只可能抛出在函数外部。
(5) throw 抛错被捕获后,会附带执行一下一条 yield 表达式。即,会附带执行一次 next 方法。

上例中 throw 抛错被捕获后,自动执行了一次 next 方法,所以会打印 b 。另外,只要 Generator 函数内部部署了 try...catch 代码块,那么遍历器 throw 方法抛出的错误,不影响下一次遍历。
(6) throw 命令与 throw 方法无关,互不影响。

上例中,throw 命令抛出的错误不会影响到遍历器的状态。所以两次执行 next 方法都进行了正确的操作。
(7) 这种函数体内捕获错误的机制,大大方便了对错误的处理。多个 yield 表达式,可以只在 Generator 函数内部写一次 try...catch 代码块来捕获错误。
(8) 一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用 next 方法,将返回一个 {value:undefined,done:true} 对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

二、Generator.prototype.return()

Generator 函数返回的遍历器对象,还有一个 return 方法,可以返回给定的值,并且终结遍历 Generator 函数。

1.return 方法参数

(1)调用 return 方法时带参数
return 方法调用时,提供参数,则返回值的 value 属性值为参数的值。

上例中,遍历器对象 g 调用 return 方法后,返回值 value 属性就是 return 方法的参数foo。并且,Generator 函数的遍历就终止了,返回值 done 的属性值为 true,之后再调用 next 方法,done 属性总是返回 true。
(2)调用 return 方法时不带参数
return 方法调用时,不提供参数,则返回值的 value 属性值为 undefined。

2.try...finally

如果 Generator 函数内部有 try...finally 代码块,且正在执行 try 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。

上例中,调用 return 方法后,就开始执行 finally 代码块,然后等到 finally 代码块执行完后,再执行 return 方法。

三、next()、throw()、return() 的共同点

next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。
(1)next() 是将 yield 表达式替换成一个值。

上例中,第二次调用 next(1) 方法就相当于将 yield 表达式替换成一个值 1。如果 next 方法没有参数,就相当于于替换成 undefined。
(2)throw() 是将 yield 表达式替换成一个 throw 语句

(3)return() 是将 yield 表达式替换成一个 return 语句

*四、yield 表达式**

如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。

上例中,foo 和 bar 都是 Generator 函数,在 bar 里面调用 foo,是不会有效果的。
这时,可以使用 yield* 表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。

另一个例子:

上例中,outer2 使用了 yield* ,outer1 没有。结果是,outer1 返回一个遍历器对象,outer2 返回该遍历器对象的内部值。

从语法角度看,如果 yield 表达式后面跟的是一个遍历器对象,需要在 yield 表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为 yield* 表达式。

如果 yield* 后面跟着一个数组,由于数组原生支持遍历器,因此会遍历数组成员。

上例中,如果 yield 命令后面不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。

实际上,任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历。

yield* 命令可以很方便的取出嵌套数组的所有成员。

五、作为对象属性的 Generator 函数

如果一个对象的属性是 Generator 函数,可以简写成下面的形式。

上例中,myGeneratorMethod 属性前面有一个星号,表示这个属性是一个 Generator 函数。
它的完整形式如下,与上面的写法是等价的:

六、Generator 函数的 this

1.Generator 函数的实例

Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的 prototype 对象上的方法。

上例表明,Generator 函数 g 返回的遍历器 obj,是 g 的实例,而且继承了 g.prototype。

2.Generator 函数不能作为构造函数

但是,如果把 g 当做普通的构造函数,并不会生效,因为 g 返回的总是遍历器对象,而不是 this 对象。

3. Generator 函数不能和 new 一起使用

Generator 函数也不能跟 new 命令一起用,会报错。

上例会报错,是因为 F 不是构造函数。

让 Generator 函数返回一个正常的对象实例,既可以用 next 方法,有可以获得正常 this 的方法:

七、含义

1.Generator 与状态机

Generator 是实现状态机的最佳结构。

上例 clock 就是一个状态机,有两种状态,每运行一次就改变一次状态。
这个函数用 Generator 实现:

上例的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量 ticking,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程思想,在写法上也更优雅。
Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。

2.Generator 与协程

协程是一种程序运行的方式,可以理解成 “协作的线程” 或 “协作的函数”。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。
(1)协程与子例程的差异
传统的 “子例程” 采用堆栈式 “后进先出” 的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态,线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。
从实现上看,在内存中,子例程只使用一个栈,而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。
(2)协程与普通线程的差异
不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
Generator 函数是ES6对协程的实现,但属于不完全实现。Generator 函数被称为 “半协程”,意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
如果将 Generator 函数当做协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用 yield 表达式交换控制权。

3. Generator 与上下文

JavaScript 代码运行时,会产生一个全局的上下文环境,包含了当前所有的变量和对象。然后,执行函数(或代码块)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈。
这个堆栈是 “后进先出” 的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。
Generator 函数不是这样,它执行产生的上下文环境,一旦遇到 yield 命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对他执行 next 命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

上例中,第一次执行 g,next() 时,Generator 函数 gen 的上下文会加入堆栈,几开始运行 gen 内部的代码。等遇到 yield 1 时,gen 上下文退出堆栈,内部状态冻结。第二次执行 g.next() 时,gen 上下文重新加入堆栈,变成当前的上下文,重新恢复执行。

八、Generator 函数的应用

1.异步操作的同步化表达

Generator 函数的暂停执行的效果,意味着可以把异步操作写在 yield 表达式里面,等到调用 next 方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 表达式下面,反正要等到调用 next 方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
例如关于数据加载:

上例中,第一次调用 loadUI 函数时,不会马上执行,仅返回一个遍历器。第一次调用 next 方法则显示loading(showLoadingScreen),并且异步加载数据(loadUIData)。数据加载完成,第二次调用 next 方法,则隐藏 loading 界面。
这样写的好处就是,所有 loading 界面的逻辑,都被封装在一个函数,按部就班非常清晰。

2.控制流管理

对于多步操作的写法:

评论 ( 0 )
最新评论
暂无评论

赶紧努力消灭 0 回复