Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,可以依次遍历Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但有两个特征。
(1)function 关键字与函数名之间有一个星号(function*)。
(2)函数体内部使用 yield 表达式,定义不同的内部状态。
Generator 函数调用方法与普通函数一样,不同的是,调用 Generator 函数后该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。必须调用遍历器对象的 next 方法,使得指针移向下一个状态返回当前指向的结果。
调用Generator 函数返回一个遍历器对象,代表 Generator 函数的内部指针。每次调用遍历器对象的 next 方法,就会返回一个有着 value 和 done 两个属相的对象。value 属性表示当前的内部状态的值。done 属性是一个布尔值,表示是否遍历结束。
Generator 函数执行后,返回一个遍历器对象,该对象本身也具有 Symbol.iterator 属性,执行后返回自身。
- yield 表达式
yield 表达式就是暂停状态。yield 表达式只能用在 Generator 函数里面,用在其他地方会 报错。
注意:yield 表达式如果用在另一个表达式中,必须放在圆括号里面。
yield 表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
- Generator 与状态机
Generator 是实现状态机的最佳结构。每运行一次,改变一次状态。
- Generator 与 协程(coroutine)
协程 是一种程序运行的方式,可以理解成"协作的线程"或"协作的函数"。协程既可以用单线程实现,也可以用多线程实现。
(1)协程与子例程的差异
传统的"子例程"(subroutine) 采用堆栈式"后进先出"的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。
协程为多个线程(单线程情况下,即多个函数)可与并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。
从实现上来看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态。
协程式以多占用内存为代价,实现多任务的并行。
(2)协程与普通线程的差异
不同:同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。
普通的线程是抢先式的,到底哪个线程先得到资源,必须由运行环境决定。但是协程是合作式的,执行权由协程自己分配。
由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用
表达式交换控制权。
yield
- Generator 与上下文
JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。
然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。
这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。
Generator 函数不是这样,它执行产生的上下文环境,一旦遇到
yield
命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行
next
命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
-
应用
(1)异步操作的同步化表达
(2)控制流管理
(3)部署Iterator 接口
(4)作为数据结构
- for...of 循环
for...of 循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,且此时不再需要调用 next 方法。
注:一旦 next 方法的返回对象的done 属性为 true,for...of 循环就会中止,且不包含该返回对象。
除了
for...of
循环以外,扩展运算符(
...
)、解构赋值和
Array.from
方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数
-
Generator.prototype.next()
yield 表达式本身是没有返回值,或者说总是返回 undefined。next 方法可以带一个参数,该参数就会当做上一个 yield 表达式的返回值。
这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过 next
方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
-
Generator.prototype.throw()
Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
Generator.prototype.throw() 方法可以接受一个参数,该参数会被 catch 语句接收,建议抛出 Error 对象的实例。
(1)若 Generator 函数内部没有部署 try...catch 代码块,那么 Generator.prototype.throw() 方法抛出的错误,将被外部 try...catch 代码块捕获。
(2)若 Generator 函数内部和外部都没有部署 try...catch ,那么程序将报错,直接中断执行。
(3)Generator.prototype.throw() 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next 方法。
第一次执行 next() 方法,等用于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时的 throw方法抛出的异常只能被函数体外的 catch 捕获。
(4)throw 方法被捕获后,会附带执行下一条 yield 表达式,即附带执行一次 next方法。只要 Generator 函数内部部署了
try...catch 代码块,那么Generator.prototype.throw() 方法抛出的错误不影响下一次遍历。
注:不要混淆遍历器对象的
throw
方法(Generator.prototype.throw())和全局的
throw
命令。遍历器对象的
throw
方法抛出的异常 Gnerator 函数体内的 catch 以及 函数体外的 catch 都可以捕获到(若 Generator 函数内部没有部署 try...catch),但全局的 throw命令 只能被函数体外的catch 语句捕获。
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用
next
方法,将返回一个
value
属性等于
undefined
、
done
属性等于
true
的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
-
Generator.prototype.return()
Generator.prototype.return() 返回给定的值,并且终结遍历Generator 函数。
若 return() 方法调用时,不提供参数,则返回值的 value 属性为 undefined。
总结:next()、throw()、return() 作用都是让 Generator 函数恢复执行,并且使用不用的语句替换 yield 表达式。
next() 是将 yield 表达式替换成一个值
throw() 是将 yield 表达式替换成一个 throw 语句
return() 是将 yield 表达式替换成一个 return 语句
-
yield* 表达式 用来在一个Generator 函数里面执行另一个 Generator函数
任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历。
yield*
命令可以很方便地取出嵌套数组的所有成员。