JS55 Generator函数
Generator函数的基本知识。
简介
Generator函数有两个特征:
function关键字后面有一个*- 函数体内部使用
yield表达式
1 | function* helloWorldGenerator() { |
调用函数后,函数并不执行,返回的也不是函数运行结果,而是指向内部状态的指针对象(即==遍历器对象==)
然后必须调用遍历器对象的next方法,让指针移向下一个状态,直到遇到yidld(或者return)为止。也就是说,yiled是暂停执行的标记,而next方法可以恢复执行:
1 | hw.next() |
每次调用nexd方法就会返回一个对象,对象有着value和done两个属性,value是yield后面的值,done是一个布尔值,表示便利是否结束。
yiled表达式
yield是函数内部的暂停标志,执行逻辑:
(1)遇到yiled暂停执行,将其后面的值作为next返回对象的value属性值
(2)下一次调用next方法,继续执行,直到遇到下一个yield
(3)如果没有新的yield则运行到return或者函数运行结束
(4)将return的值作为返回对象的value属性值,如果没有return,返回对象的value属性值为undeinfed
yield提供了==惰性求值==的功能。
==yield表达式只能用在Generator函数里面,用在其他地方都会报错==。
与Iterator接口的关系
可以将Generator函数赋值给对象的Symbol.iterator属性,从而使得对象具有Iterator接口
1 | let obj = {}; |
next方法
==yield表达式本身没有返回值==或者说总是返回undefined(这个指的是它在内部对于本身的传递,而非传递给next返回对象的value的属性值)
1 | function* f() { |
a的值是undefined
next方法可以带一个参数,这个参数会被当做==上一个==yield的表达式的返回值。
1 | function* f() { |
这个功能,可以在Generator函数开始运行后,==从外部向函数体内部注入值==,从而调整函数行为。
注意,==next注入的参数改变的是yield表达式的返回值==:
1 | function* foo(x) { |
当执行b.next(12)时,不是给y赋值12,而是yield (x + 1)为12,所以value是8
上面提到了,next的参数是赋值给==上一个==yield表达式返回值,所以在首次调用next传参是==无效==的。
==第一次执行next方法,等同于启动执行Generator函数的内部代码==
for...of循环
for...of循环可以自动遍历Generator生成的Iterator对象,不需要再逐步调用next方法
1 | function* foo() { |
要注意的是,==当nextd方法的返回值的done属性为true时,for...of循环就会终止,并且不包括return的值==
除了for...of之外,扩展运算符、解构赋值、Array.from内部都调用的遍历器接口,都可以将Generator函数返回的Iterator对象作为参数。
1 | function* numbers () { |
Generator.prototype.throw()
Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获
1 | function* f() { |
遍历器对象抛出的错误被Generator函数体内捕获后,Generator函数try语句内的其他语句就==不再继续运行==,并且遍历器对象抛出的其他错误也==不会被Generator函数内捕获==,而是被全局的catch捕获
try语句中抛出错误,try中的其他语句不会在继续执行
==不要混淆遍历器对象的throw方法和全局的throw命令==
如果函数内部没有部署try...catch代码,遍历器对象抛出的错误会被外部的额try...catch代码捕获
要注意的是,throw抛出的错误要被内部捕获,前提是==必须执行过一次next方法==,因为不执行一次next代码,意味着Generator函数没有启动执行,所以错误会被抛出在函数外部。
遍历器对象的throw方法被捕获后,自动执行了一次next方法,并且只要Generator函数内部部署了try...catch代码,throw方法也不会影响下一次遍历
1 | function* f() { |
这种在==函数体内==捕获错误的机制,大大方便了错误的处理,多个yield表达式,可以在函数内部==使用一个try...catch代码块==来捕获错误就可以了。
Generator函数内部的错误,也可以被函数体外的catch捕获,但是==函数内部的代码就不会再继续执行了==,JavaScript认为这个Generator已经==结束运行==了,再调用next方法会返回一个value属性为undefined,done属性为true的对象
Generator.prototype.return()
Generator函数返回的遍历器对象有return方法,可以返回给定的值,提前==结束==Generator函数。
1 | function* f() { |
return不提供参数,返回值的value属性是undefined。
如果Generator内部有try...finally代码块,且正在执行try代码,return方法会推迟到finally代码块执行完再执行
next、throw、return的共同点
三者都是让Generator函数恢复执行,并且使用不同的语句替换yield表达式
next是将yield表达式替换为一个值,throw是将yield表达式替换成throw语句,return是将yield表达式替换为return语句
1 | const g = function* (x, y) { |
next:
1 | gen.next(1); // Object { value: 1, done: true } |
throw:
1 | gen.throw(new Error('出错了')); // Uncaught Error: 出错了 |
return:
1 | gen.return(2); // Object { value: 2, done: true } |
yield*表达式
如果在Generator函数内部调用另外一个Generator函数,默认情况下是无效的
1 | function* foo() { |
==在Generator函数内部调用另外一个Generator函数需要用到yield*表达式==:
1 | function* foo() { |
yield*后面的Generator函数(没有return语句时)等同于在Generator函数内部部署一个for...of循环
1 | function* concat(iter1, iter2) { |
有return语句时,可以通过赋值var value = yield* iterator获取return语句的值,==yield*后面表达式中的return语句作为一个遍历的结果,而不是作为yield*的的返回值==
1 | function* foo() { |
如果yiled*后面跟着一个数组,会直接遍历这个数组:
1 | function* foo() { |
实际上,==任何数据结构只要有Iterator接口,就可以被yield*遍历==。
1 | const read = (function* () { |
作为对象属性的Generator函数
可以简写为下面的形式:
1 | let obj = { |
Generator函数的this
Generator函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例:
1 | function* gen(){} |
但是==Generator不能作为构造函数使用==,因为它的返回值总是一个遍历器对象,而非this对象(即使显示声明return this也不可以)
1 | function* gen(){ |
Generator函数也不能和new一起使用,会报错。
Generator与协程
协程有多个线程(函数),可以并行执行,但是只有一个线程(函数)处在正在运行的状态,其他线程(函数)都处于暂停状态(suspended),线程(函数)之间可以交换控制权。
协程以多占用内存为代价,实现多任务的并行。
Generator函数是ES6对协程的==不完全==实现,成为“半协程”,只有Generator函数的调用者才有权将程序的执行权还给Generator函数(完全协程,任何函数都可以将暂停的协程继续执行)
如果将Generator函数当作协程,可以将多个需要相互协作的任务写作Generator函数,之间==使用yield表达式交换控制权==。
练习 & 应用
判断Generator函数输出结果1
1 | function* dataConsumer() { |
思路:next的参数是对上一次的yield表达式返回值赋值,所以拆开来看:
1 | function* dataConsumer() { |
判断Generator函数输出结果2
1 | function* g() { |
Generator函数内部的错误,如果没有被内部捕获,则会被外部捕获,这时候Generator函数执行完毕,不再继续执行
注意,在内部抛出错误后,next返回值仍为上一次的返回值,并非抛出的结果。
结果:
1 | // starting generator |
判断Generator函数输出结果3
1 | function* foo() { |
要注意的是,被代理的Generator函数的return语句,不再作为next方法的输出结果,而是用来向代理它的Generator函数返回数据
1 | // { value: 1, done: false } |
判断Generator函数输出结果4
1 | function* genFuncWithReturn() { |
需要好好判断顺序,首先genFuncWithReturn()返回了一个迭代器对象,然后传入了logReturned中,按顺序执行,执行了console.log(result)之后,才会执行解构操作,所以顺序是:
1 | // The result |
如果换一种形式输出结果就不同了:
1 | for (let i of logReturned(genFuncWithReturn())) { |
关键点就是解构操作符是等到函数执行后再执行的。
让for...of可以遍历原生对象
原生的JavaScript对象时候不能使用for...of进行遍历的,因为并没有部署遍历接口。
1 | let obj = { |
使用Generator函数,for...of可以遍历原生对象。
思路:由于原生对象没有部署遍历器接口,所以需要为对象的遍历器接口部署一个Generator函数,返回一个遍历器对象
1 | obj[Symbol.iterator] = function* () { |
可以编写一个更通用的方法:
1 | function makeIterator (obj) { |
我们之所以能够使用上面的Generator函数,就是因为它的返回结果是一个Iterator对象,这个Iterator对象有next方法,每次遍历时都要调用这个方法,返回的记结果就是包含了value和done两个属性的值
所以,我们不使用Generator函数,自己都构造返回一个具有next方法的对象也是可以的,next方法返回对象也需要包括了value和done连个属性,value属性是for...of的返回值,done用来标识遍历何时结束。
1 | function makeIterator (obj) { |
第一次调用next方法就能够传值
next的参数是赋值给==上一个==yield表达式返回值,所以在首次调用next传参是==无效==的。
构造一个wrapper函数,返回一个Generator函数,实现在第一次调用next方法时就能够输入值。
1 | const wrapped = wrapper(function* () { |
思路:既然Generator首个next不能传参,那么就在我们的包裹函数中,将首次next调用在包裹函数内执行
1 | const wrapper = function(fn) { |
利用Generator函数和for...of循环,实现斐波那契数列
1 | function* fibonacci() { |
实现一个clock状态机
如果不使用Generator函数:
1 | const clock = (function () { |
使用Generator函数:
1 | const clock = (function* () { |
输出多维数组中的值
1 | const numbers = flatten2([1, [[2], 3, 4], 5]) |
思路就是递归调用Generator函数:
1 | function* flatten(arr) { |
要注意的就是,在一个Generator函数里面调用另外一个Generator函数,默认是无效的,所以必须使用yield*表达式来调用
遍历二叉树
对二叉树这里有些迷糊,因为基础不牢固,回头好好不玩了数据结构和算法,再来重新看一下这里(2019.01.17)
1 | // 下面是二叉树的构造函数, |
控制流管理
如果一个多步操作非常耗时,采用回调函数,可能会写成下面这样:
1 | step1(function (value1) { |
改写成Promise格式:
1 | Promise.resolve(step1) |
改写成Generator格式:
1 | function* gen(value1) { |
需要一个函数按次序调用:
1 | scheduler(longRunningTask(initialValue)); |
上面这种做法,只适合同步操作,即所有的task都必须是同步的,不能有异步操作。
让Generator函数能够使用new
Generator函数不能和new命令一起用,会报错:
1 | function* F() { |
如何让Generator函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this?
1 | function* gen () { |
实现:
1 | function F (){ |
实际上这是一个有欺骗性的做法,实际上new关键字无效的,我们要的只是执行F即可。
1 | let f = new F(); |
而gen.call(gen.prototype)相当于在gen原型上添加了属性,当访问f.a时实际上访问的就是原型链上的属性。