ES6学习10(Generator)
来源:互联网 发布:域名 购买 编辑:程序博客网 时间:2024/05/14 05:54
简介
Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
形式上,Generator函数是一个普通函数,但是有两个特征
- function关键字与函数名之间有一个星号
- 函数体内部使用yield语句,定义不同的内部状态
我们先来看一个例子:
function* helloWorldGenerator() { for (var i = 5; i >= 0; i--) { yield i; } return 'finished';}var hw = helloWorldGenerator();console.log(hw.next());//{ value: 0, done: false }console.log(hw.next());//{ value: 1314, done: false }console.log(hw.next());//{ value: 5201314, done: true }console.log(hw.next());//{ value: undefined, done: true }
可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。
Generator函数的调用方法与普通函数一样。不同的是,调用Generator函数后,该函数并不执行,而是返回一个遍历器对象。
接下来,调用遍历器对象的next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止,并输出yield或return语句后面的表达式的值与是否遍历完成的标志。
也就是说,Generator函数是分段执行的,分段的标记就是yield语句。
yield
Generator实际上提供了一种可以暂停执行的函数,yield语句就是暂停标志。
遍历器对象的next方法的运行逻辑如下:
- 遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
- 下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。
- 如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
- 如果该函数没有return语句,则返回的对象的value属性值为undefined。
yield语句后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”的语法功能。
yield语句不能用在普通函数中,否则会报错。
yield语句如果用在一个表达式之中,必须放在圆括号里面。
yield句本身没有返回值,或者说总是返回undefined。
与Iterator接口的关系
由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。
var myIterable = {};myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3;};[...myIterable] // [1, 2, 3]
next方法的参数
yield句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
Generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。
function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z);}var a = foo(5);console.log(a.next()) // Object{value:6, done:false}console.log(a.next()) // Object{value:NaN, done:false}console.log(a.next()) // Object{value:NaN, done:true}var b = foo(5);//这句next调用后,函数停在了yield (x + 1)这里。console.log(b.next()) // { value:6, done:false }//执行这句next时指定了参数,就意味着将yield (x + 1)的返回值设置为了12//继续执行时就会执行var y = 2 * 12;console.log(b.next(12)) // { value:8, done:false }console.log(b.next(13)) // { value:42, done:true }
for…of循环
for…of循环可以自动遍历Generator函数时生成的Iterator对象,且此时不再需要调用next方法。
function *foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6;}for (let v of foo()) { console.log(v);}// 1 2 3 4 5
一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for…of循环之中。
利用for…of循环,可以写出遍历任意对象(object)的方法。
function* objectEntries() { let propKeys = Object.keys(this); for (let propKey of propKeys) { yield [propKey, this[propKey]]; }}let jane = { first: 'Jane', last: 'Doe' };jane[Symbol.iterator] = objectEntries;for (let [key, value] of jane) { console.log(`${key}: ${value}`);}// first: Jane// last: Doe
Generator.prototype.throw()
Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。
var g = function* () { try { yield '如果不先执行一次next就直接调用throw抛出的错误是不会被内部捕获到的,因为抛出错误时函数其实并没有进到这个try里。'; } catch (e) { console.log('内部捕获1', e); }};var i = g();try { console.log(i.throw('cant catch inside'));} catch (e) { console.log('外部捕获1', e);}//一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。//如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象//即JavaScript引擎认为这个Generator已经运行结束了。console.log(i.next());//外部捕获1 cant catch//Object { value: undefined, done: true }
var g = function* () { try { yield '如果不先执行一次next就直接调用throw抛出的错误是不会被内部捕获到的,因为抛出错误时函数其实并没有进到这个try里。'; } catch (e) { console.log('内部捕获1', e); } try { yield 'throw方法会顺便执行一次next'; } catch (e) { console.log('内部捕获2', e); } return '这次的错误没有在内部捕获,就会传到外部';};var i = g();// try {// console.log(i.throw('cant catch inside'));// } catch (e) {// console.log('外部捕获1', e);// }console.log(i.next());try { console.log(i.throw('a')); console.log(i.throw('b')); console.log(i.throw('c'));} catch (e) { console.log('外部捕获2', e);}// Object { value: "如果不先执行一次next就直接调用throw抛出的错误是不会被内部捕获…", done: false }//内部捕获1 //Object { value: "throw方法会顺便执行一次next", done: false }//内部捕获2 b//Object { value: "这次的错误没有在内部捕获,就会传到外部", done: true }//外部捕获2 c
Generator.prototype.return()
Generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历Generator函数。
function* gen() { yield 1; yield 2; yield 3;}var g = gen();console.log(g.next()) // { value: 1, done: false }console.log(g.return('foo')) // { value: "foo", done: true }console.log(g.next()) // { value: undefined, done: true }
如果Generator函数内部有try…finally代码块,那么调用return方法会立即去执行finally代码块中的代码,执行完再返回return方法指定的值和循环完的标志。
function* numbers () { yield 1; try { yield 2; yield 3; } finally { yield 4; yield 5; } yield 6;}var g = numbers()console.log(g.next()) // { done: false, value: 1 }console.log(g.next()) // { done: false, value: 2 }console.log(g.return(7)) // { done: false, value: 4 }console.log(g.next()) // { done: false, value: 5 }console.log(g.next()) // { done: true, value: 7 }
yield*语句
如果在Generater函数内部,调用另一个Generator函数,默认情况下是没有效果的。
这个就需要用到yield*语句,用来在一个Generator函数里面执行另一个Generator函数。
如果被代理的Generator函数有return语句,那么就可以向代理它的Generator函数返回数据。
function* foo() { yield 'a'; yield 'b'; return "Generator 的返回值";}function* bar() { yield 'x'; console.log(foo());//Generator对象 console.log(yield foo());//undefined 普通的yield获取不到foo的返回值 console.log((yield* foo())+'in');//Generator 的返回值in yield*获取的到foo的返回值,这个返回值在yield里不会再被遍历到 yield 'y';}for (let v of bar()){ console.log(v);}//x//Generator { }//Generator { }//undefined//a//b//Generator 的返回值in//y
对于那些有原生遍历器接口的yield*可以直接遍历:
function* gen(){ yield* ["a", "b", "c"]; yield ["a", "b", "c"]; yield* 'hello';}for (var i of gen()) { console.log(i);}/*abcArray [ "a", "b", "c" ]hello*/
利用yield*语句取出嵌套数组
function* iterTree(tree) { if (Array.isArray(tree)) { for(let i=0; i < tree.length; i++) { yield* iterTree(tree[i]); } } else { yield tree; }}const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];for(let x of iterTree(tree)) { console.log(x);}// a// b// c// d// e
使用yield*语句遍历完全二叉树
首先来看看我们的数的结构:
// 三个参数分别是左树、当前节点和右树function Tree(left, label, right) { this.left = left; this.label = label; this.right = right;}// 下面生成二叉树function make(array) { // 判断是否为叶节点 if (array.length == 1) return new Tree(null, array[0], null); return new Tree(make(array[0]), array[1], make(array[2]));}let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
普通的中序遍历:
// 普通遍历二叉树var res = [];function re(root) { if (root) { re(root.left); res.push(root.label); re(root.right); }}re(tree);console.log(res);
使用yield遍历:
// 下面是中序(inorder)遍历函数。// 由于返回的是一个遍历器,所以要用generator函数。// 函数体内采用递归算法,所以左树和右树要用yield*遍历function* inorder(t) { if (t) { yield* inorder(t.left); yield t.label; yield* inorder(t.right); }}// 遍历二叉树var result = [];for (let node of inorder(tree)) { result.push(node);}console.log(result);// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
Generator函数的this
Generator函数比较特殊,他总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法。
不能读取自己的this
但是由于Generator函数总是返回遍历器对象而不是自己本身,把这个函数当做构造函数用是不行的。
function* g() { this.a = 1; console.log("done");}g.prototype.hello = function () { return 'hi!';};let obj = g();obj.next();console.log(obj instanceof g) // trueconsole.log(obj.hello()) // 'hi!'console.log(obj.a) // 'undefined'
如果你想同时使用this和遍历器可以用另一个对象绑在Generator函数的this上,如果新建一个对象来绑显然不太方便使用,将Generator函数的prototype绑上就可以在本对象上又使用this又使用遍历器了:
function* F() { this.a = 1; yield this.b = 2; yield this.c = 3;}var f = F.call(F.prototype);console.log(f.next()); // Object {value: 2, done: false}console.log(f.next()); // Object {value: 3, done: false}console.log(f.next()); // Object {value: undefined, done: true}console.log(f.a) // 1console.log(f.b) // 2console.log(f.c) // 3
不能使用new命令
Generator函数也不能跟new命令一起用,会报错。
function* F() { yield this.x = 2; yield this.y = 3;}new F()// TypeError: F is not a constructor
你要非想用,就包一层吧:
function* gen() { this.a = 1; yield this.b = 2; yield this.c = 3;}function F() { return gen.call(gen.prototype);}var f = new F();console.log(f.next()); // Object {value: 2, done: false}console.log(f.next()); // Object {value: 3, done: false}console.log(f.next()); // Object {value: undefined, done: true}console.log(f.a) // 1console.log(f.b) // 2console.log(f.c) // 3
意义
Generator与状态机
Generator是实现状态机的最佳结构。
假如一个clock有两个状态:
var clock = function*() { while (true) { console.log('Tick!'); yield; console.log('Tock!'); yield; }};var li = clock();li.next();//Tickli.next();//Tockli.next();//Tickli.next();//Tock
如果要用ES5来实现就要有一个标志变量来保存当前的状态。
这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。
应用
异步操作的同步化表达
比如Ajax,我们需要等回调,把对返回数据的处理放在回调函数中,但是有了Generator就不同了
function* main() { //同步方式编写逻辑 var result = yield request("http://some.url"); var resp = JSON.parse(result); console.log(resp.value);}function request(url) { makeAjaxCall(url, function(response){ //数据返回后将数据作为yield的返回值传到Generator里 it.next(response); });}//初始化var it = main();//发起Ajax请求it.next();
控制流管理
如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); });});
使用Generator函数可以改善这一点:
function* longRunningTask(value1) { try { var value2 = yield step1(value1); var value3 = yield step2(value2); var value4 = yield step3(value3); var value5 = yield step4(value4); // Do something with value4 } catch (e) { // Handle any error from step1 through step4 }}function scheduler(task) { var taskObj = task.next(task.value); // 如果Generator函数未结束,就继续调用 if (!taskObj.done) { task.value = taskObj.value scheduler(task); }}scheduler(longRunningTask(initialValue));
或者更一般的办法:
let steps = [step1Func, step2Func, step3Func];function *iterateSteps(steps){ for (var i=0; i< steps.length; i++){ var step = steps[i]; yield step(); }}
部署Iterator接口
利用Generator函数,可以在任意对象上部署Iterator接口。
function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; }}let myObj = { foo: 3, bar: 7 };for (let [key, value] of iterEntries(myObj)) { console.log(key, value);}
- ES6学习10(Generator)
- ES6学习笔记:Generator
- es6 Generator (十五)
- ES6学习—Generator函数
- 学习笔记:ES6之Generator
- ES6个人学习整理(七)——Generator
- ES6学习笔记(七)--Generator函数与Promise对象
- es6 之 Generator(一)
- es6 之 Generator(二)
- ES6之生成器(Generator)
- ES6--Generator
- ES6-generator
- 【es6】Generator
- ES6 Generator
- ES6 Generator
- ES6学习12章:Generator函数
- es6 javascript 的Generator 函数 (上)
- es6 javascript 的Generator 函数 (下)
- 如何使用 Xcode8 进行开发调试
- Material Design下面的Toolbar的一些使用
- JVM性能调优监控工具jps、jstack、jmap、jhat、jstat使用详解
- SimulateIDFA,新一代iOS设备的广告追踪解决方案
- jedis操作redis
- ES6学习10(Generator)
- 移动端的自适配/js控制
- BZOJ 1878: [SDOI2009]HH的项链 树状数组+乱搞
- 1024程序员节是什么节?程序员又是干什么的?
- CVPR 2016-10-14
- [Java 8] (1) 函数式编程简介
- easyUI之表单验证validate
- 超实用的中国省市区PList文件
- 个人中心的角标问题