依赖文件地址 :https://github.com/chanceLe/ES6-Basic-Syntax/tree/master/js
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>[es6]-14-Generator函数</title> 6 <script src="./js/browser.js"></script> 7 <script src="./js/babel-pollyfill.js"></script> 8 <!--引入babel-pollyfill的原因是,默认的babel不支持Generator--> 9 <!--<script src="https://cdn.bootcss.com/babel-polyfill/7.0.0-alpha.9/polyfill.min.js"></script>--> 10 <script type="text/babel"> 11 /* 12 * Generator函数有多种理解角度。 13 * 从语法上,首先可以理解成是一个状态机,封装了多个内部状态。 14 * 15 * 执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机, 16 * 还是一个遍历器对象生成函数。返回的遍历器对象,可依次遍历Generator函数内部的每一个状态。 17 * 18 * 形式上 Generator函数是一个普通函数,但是有两个特征。 19 * 1.function关键字与函数名之间有一个*号。 20 * 2.函数体内部使用yield语句,定义不同的内部状态(yield在英文里是产出的意思)。 21 */ 22 23 function* helloWorldGenerator(){ 24 yield "hello"; 25 yield "world"; 26 return "ending"; 27 } 28 var hw=helloWorldGenerator(); 29 //上面定义了一个Generator函数,有三个状态。调用后,并不执行,返回的也不是函数的运行结果 30 // 而是一个指向内部状态的指针,返回的是遍历器对象。 31 //必须调用next方法,使指针移向下一个状态。 32 console.log(hw.next()); 33 console.log(hw.next()); 34 console.log(hw.next()); 35 /* 36 * 每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield或 37 * return语句为止。换言之,Generator是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。 38 */ 39 40 /* 41 * 遍历器对象的next方法的运行逻辑: 42 * 1.遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。 43 * 2.下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。 44 * 3.如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为 45 * 返回对象的value属性值。 46 * 4.如果该函数没有return语句,则返回的对象的value属性值为undefined。 47 * 48 * 要注意:yield语句后面的表达式,只有当调用next方法,内部指针指向该语句时才会执行,因此等于为js提供了手动的 49 * 惰性求值的语法功能。 50 */ 51 52 /* 53 * Generator函数可以不用yield语句,这时就变成了一个单纯的暂缓执行函数。 54 */ 55 function* f(){ 56 console.log("执行了"); 57 } 58 var generator = f(); 59 60 setTimeout(()=>{generator.next()},2000) 61 62 //普通函数不能用yield语句,会报错。多用在有些方法的参数是普通函数。 63 //yield语句用在一个表达式中,必须放在圆括号里面。 64 65 // console.log("hello" + (yield)) //报错Unexpected strict mode reserved word 66 //yield语句用作函数参数或赋值表达式的右边,可以不加括号。 67 68 /* 69 * 与iterator的关系 70 * 之前说过,任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数 71 * 会返回该对象的一个遍历器对象。 72 * 由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性, 73 * 从而使该对象具有Iterator接口。 74 */ 75 var myIterator = {}; 76 myIterator[Symbol.iterator] = function* (){ 77 yield 1; 78 yield 2; 79 yield 3; 80 } 81 console.log([...myIterator]) //[1,2,3] 82 83 //Generator函数执行后,返回一个遍历器对象。该对象本身也有一个Symbol.iterator 属性,执行后返回自身。 84 function* gen(){ 85 //somecode 86 } 87 var g = gen(); 88 89 console.log(g[Symbol.iterator]() === g) //true 执行后返回自身 90 91 /* 92 * next方法的参数 93 * yield本身没有返回值,或者说返回值是undefined。 94 * next方法可以带一个参数,该参数就会被当做上一个yield语句的返回值。 95 */ 96 97 function* para(){ 98 for(let i=0;true;i++){ 99 var reset = yield i; 100 if(reset){ 101 i=-1; 102 } 103 } 104 }; 105 var pa =para(); 106 console.log(pa.next()); 107 console.log(pa.next()); 108 console.log(pa.next(true)); 109 console.log(pa.next()); 110 console.log(pa.next()); 111 /* 112 * 这个参数有很重要的语法意义,Generator函数从暂停到恢复运行,它的上下文状态 113 * 是不变的。通过next的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。 114 * 也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的值。从而调整函数行为。 115 */ 116 117 118 function* foo(x){ 119 var y = 2*(yield (x+1)); 120 var z = yield (y / 3); 121 return (x+y+z); 122 } 123 var m = foo(5); 124 // console.log(m.next()); 125 // console.log(m.next()); 126 // console.log(m.next()); 127 /* 结果 128 * Object {value: 6, done: false} 129 Object {value: NaN, done: false} 130 Object {value: NaN, done: true} 131 132 第二次运行的时候,next不带参数,导致y的值等于2*undefined(即NaN),除以3以后还是NaN。 133 第三次运行的时候不带参数,z等于undefined,返回对象的value属性等于5+NaN+undefined 即NaN。 134 135 加上参数的运行结果完全不同: 136 * */ 137 138 console.log(m.next()); 139 console.log(m.next(12)); 140 console.log(m.next(13)); 141 /* 142 * Object {value: 6, done: false} 143 Object {value: 8, done: false} 144 Object {value: 42, done: true} 145 */ 146 147 /* 148 * 注意由于next方法的参数表示的是上一个yield语句的返回值,所以第一次使用next方法不能带参数。v8引擎 149 * 直接忽略第一次使用next方法的参数。 150 * 151 * 如果想要第一次调用next时,就能够输入值,可以在Generator函数外面再包一层: 152 */ 153 154 function wrapper(generatorFunction){ 155 return function(...args){ 156 let generatorObj = generatorFunction(...args); 157 generatorObj.next(); 158 return generatorObj; 159 } 160 } 161 const wrapped = wrapper(function*(){ 162 console.log(`First input:${yield}`); 163 return 'Done'; 164 }) 165 166 wrapped().next("hello!"); 167 168 //再看一个通过next方法,向Generator函数内部输入值的例子: 169 function* dataConsumer(){ 170 console.log("Strated!"); 171 console.log(`1.${yield}`) 172 console.log(`2.${yield}`) 173 } 174 let dataObj = dataConsumer(); 175 dataObj.next(); //strarted 176 dataObj.next("a"); //a 177 dataObj.next("b"); //b 178 179 //for...of循环可以自动遍历Generator函数生成的Iterator对象,且不再需要next方法。 180 function*foo2(){ 181 yield 1; 182 yield 2; 183 yield 3; 184 yield 4; 185 return 5; 186 } 187 for(let v of foo2()){ 188 console.log(v); 189 } //1 2 3 4 190 //z这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象。 191 //所以上面代码的return 语句返回的5,并不包括在for...of之中。 192 //下面是Generator函数结合for...of循环,实现斐波那契数列的例子: 193 function *fibonacci(){ 194 let [prev,curr] = [0,1]; 195 for(;;){ 196 [prev,curr] = [curr,prev + curr]; 197 yield curr; 198 } 199 } 200 201 for(let n of fibonacci()){ 202 if(n>1000) break; 203 console.log(n); 204 } 205 //利用for...of循环,可以写出遍历任意对象的方法。原生js对象没有遍历接口,无法使用for...of,通过Generator 206 //加上这个接口就可以了。 207 208 function *objectEntries(obj){ 209 let propKeys = Reflect.ownKeys(obj); 210 211 for(let propKey of propKeys){ 212 yield [propKey,obj[propKey]]; 213 } 214 } 215 let jane = {first:"jane",last:"doe"} 216 for(let [key ,value] of objectEntries(jane)){ 217 console.log(`${key},${value}`); 218 } //first,jane last,doe 219 220 //加上遍历器接口的另一种写法,将Generator函数加到对象的Symbol.iterator属性上面。 221 222 function* objectEntries2(){ 223 224 let propKeys = Object.keys(this); 225 226 for(let propKey of propKeys){ 227 yield [propKey,this[propKey]]; 228 } 229 } 230 let jane2 = {first:"jane2",last:"doe2"}; 231 jane2[Symbol.iterator] = objectEntries2; 232 233 for(let [key ,value] of jane2){ 234 console.log(`${key},${value}`); 235 } 236 237 //除了for...of以外,扩展运算符,结构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着, 238 //他们都可以将Generator返回的Iterator对象,作为参数。 239 240 function* numbers(){ 241 yield 1; 242 yield 2; 243 return 3; 244 yield 4; 245 } 246 //扩展运算符 247 console.log([...numbers()]) //[1,2] 248 //Array.from方法 249 console.log(Array.from(numbers())); // [1,2] 250 //解构赋值 251 let [x,y] = numbers(); 252 console.log(x,y); //1 2 253 //for...of循环 254 for(let m of numbers()){ 255 console.log(m); //1 2 256 } 257 258 259 //Generator.prototype.throw() 260 //Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。 261 var g = function*(){ 262 try{ 263 yield; 264 }catch(e){ 265 console.log("内部捕获",e); 266 } 267 } 268 var i = g(); 269 i.next(); 270 try{ 271 i.throw("a"); 272 i.throw("b"); 273 }catch(e){ 274 console.log("外部捕获",e); 275 } 276 277 //上面代码中,遍历器对象i连续抛出两个错误。第一个被Generator函数体内的catch语句捕获。i第二次抛出错误, 278 //由于Generator函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误抛出了函数体,被函数体外的 279 //catch捕获。 280 281 //throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error实例。 282 //注意区分遍历器的throw和全局命令throw。全局的是无法被遍历器对象捕获的。 283 //throw方法被捕获以后会附带执行下一条yield语句。也就是说会执行一次next方法。 284 //Generator函数体外抛出的错误,可以在函数体内捕获,反过来,函数体内抛出的错误,也可以在函数体外的catch捕获。 285 286 function* foo3(){ 287 var x = yield 1; 288 var y = x.toUpperCase(); 289 yield y; 290 } 291 var it = foo3(); 292 console.log(it.next()); 293 try{ 294 it.next(45); 295 }catch(e){ 296 console.log(e); 297 } 298 299 //一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去,如果此后还调用next方法,将返回一个value 300 //属性等于undefined,done属性等于true的对象,即js引擎认为这个Generator已经运行结束了。 301 302 303 //Generator.prototype.return 304 //Generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历Generator函数。 305 function* foo3(){ 306 yield 1; 307 yield 2; 308 yield 3; 309 } 310 var hu = foo3(); 311 console.log(hu.next()); // 1 312 console.log(hu.return("haha")); //haha 313 console.log(hu.next()); //undefined 314 315 //如果return方法调用时,不提供参数,则返回值的value属性为undefined。 316 //如果Generator函数内部有try...finally代码,return会推迟到finally代码块执行完再执行。 317 318 function * numbers2(){ 319 yield 1; 320 try{ 321 yield 2; 322 yield 3; 323 }finally{ 324 yield 4; 325 yield 5; 326 } 327 yield 7; 328 } 329 var g = numbers2(); 330 console.log(g.next()); //1 331 console.log(g.next()); //2 332 console.log(g.return(7)); //4 开始执行finally 333 console.log(g.next()); //5 334 console.log(g.next()); //7 执行完finally 执行return 结束 335 console.log(g.next()); //已经结束。 undefined 336 337 //yield*语句 338 //如果在Generator函数内部,调用另一个Generator函数,默认情况下是没有效果的。 339 //yield*语句,用来在一个Generator函数里面执行另一个Generator函数。 340 function*bar(){ 341 yield "a"; 342 yield "b"; 343 } 344 function*baz(){ 345 yield "m"; 346 yield* bar(); 347 yield "n"; 348 } 349 for(let v of baz()){ 350 console.log(v); // m a b n 351 } 352 /* 353 * yield*后面的Generator函数(没有return语句时),等同于在Generator函数内部部署一个for...of循环。 354 * 如果有return,需要用var value = yield* iterator的形式获取return的值。 355 * 356 * 如果yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。 357 * yield后面如果不加*号,返回的是整个数组,加上*号表示返回的数组的遍历器对象。 358 */ 359 360 //下面是使用yield*语句遍历完全二叉树 361 { 362 //二叉树构造函数 363 function Tree(left,label,right){ 364 this.left = left; 365 this.right = right; 366 this.label = label; 367 } 368 //中序遍历 369 function* inorder(t){ 370 if(t){ 371 yield* inorder(t.left); 372 yield t.label; 373 yield* inorder(t.right); 374 } 375 } 376 //生成二叉树 377 function make(array){ 378 if(array.length == 1){ 379 return new Tree(null,array[0],null); 380 } 381 return new Tree(make(array[0]),array[1],make(array[2])); 382 } 383 let tree = make([[["a"],"b",["c"]],"d",[["e"],"f",["g"]]]); 384 //遍历二叉树 385 var result = []; 386 for(let node of inorder(tree)){ 387 result.push(node); 388 } 389 390 console.log(result); 391 //["a","b","c","d","e","f","g"] 392 } 393 394 395 //作为对象属性的Generator函数,可以简写成下面的形式 396 { 397 let obj = { 398 *myGenerator(){ 399 //code 400 } 401 } 402 } 403 404 //Generator函数的this 405 //Generator函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的 406 //prototype对象上的方法。 407 { 408 function* g(){} 409 g.prototype.hello = function(){ 410 console.log("hi!"); 411 } 412 let obj = g(); 413 414 console.log(obj instanceof g); //true 415 obj.hello(); //hi 416 } 417 //上面代码表明,Generator函数g返回的遍历器obj,是g的实例,而且继承了g.prototype. 418 //但是如果把g当做普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象、 419 { 420 function* g(){ 421 this.a = 11; 422 } 423 let obj = g(); 424 console.log(obj.a); //undefined 425 } 426 // 上面代码中,Generator函数g在this对象上添加了一个属性a,但是obj对象拿不到这个属性。 427 428 //有一个变通的方法,让Generator函数返回一个正常的对象示例,既可以用next方法,又可以正常获得this: 429 { 430 function *F(){ 431 this.a = 1; 432 yield this.b = 2; 433 yield this.c = 3; 434 } 435 var obj = {}; 436 var f = F.call(obj); 437 438 console.log(f.next()); //2 439 console.log(f.next()); //3 440 console.log(f.next()); //undefined 441 442 console.log(obj.a); //1 443 console.log(obj.b); //2 444 console.log(obj.c); //3 445 } 446 447 //上面代码中,执行的是遍历器对象f,生成的对象实例是obj,让他们统一的一个办法是将obj换成F.prototype。 448 { 449 function *F(){ 450 this.a = 1; 451 yield this.b = 2; 452 yield this.c = 3; 453 } 454 455 var f = F.call(F.prototype); 456 457 console.log(f.next()); //2 458 console.log(f.next()); //3 459 console.log(f.next()); //undefined 460 461 console.log(f.a); //1 462 console.log(f.b); //2 463 console.log(f.c); //3 464 } 465 //再将F改成构造函数,就可以对它执行new命令了 466 { 467 function *gen(){ 468 this.a = 1; 469 yield this.b = 2; 470 yield this.c = 3; 471 } 472 function F(){ 473 return gen.call(gen.prototype); 474 } 475 var f = new F(); 476 477 console.log(f.next()); //2 478 console.log(f.next()); //3 479 console.log(f.next()); //undefined 480 481 console.log(f.a); //1 482 console.log(f.b); //2 483 console.log(f.c); //3 484 } 485 486 //含义 487 //1.Generator与状态机 488 //Generator是实现状态机的最佳结构。比如下面的clock函数就是一个状态机。 489 { 490 var ticking = true; 491 var clock = function(){ 492 if(ticking){ 493 console.log("Tick!"); 494 }else{ 495 console.log("Tock!") 496 } 497 ticking = !ticking; 498 } 499 clock(); 500 clock(); 501 } 502 //上面代码的clock函数一共有两种状态(tick和tock),每运行一次,改变一次状态。这个函数如果用Generator实现,就是下面这样。 503 { 504 var clock = function*(){ 505 while(true){ 506 console.log("Tick!") 507 yield; 508 console.log("Tock!") 509 yield; 510 } 511 } 512 513 var c = clock(); 514 c.next(); 515 c.next(); 516 } 517 518 //上面的Generator实现,比ES5的实现,少了外部变量,这样更安全(状态不会被非法改变),简洁。 519 //也更 符合函数式编程的思想。 520 521 522 //Generator可以暂停函数执行,返回任意表达式的值。这种特点使得Generator有多种应用场景。 523 /* 524 * 1.异步操作的同步化表达。 525 * Generator函数的暂停执行效果,意味着可以把异步操作写在yield语句里面,等到调用next方法时,再往后执行。 526 * 这实际上等同于不需要写回调函数了。因为异步操作的后序操作可以放在yield语句下面,反正要等到调用next方法时才执行。 527 * 所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。 528 * 用读取文本的例子: 529 { 530 531 function* numbers(){ 532 let file = new FileReader("./[es6]-02-块级作用域.html"); 533 try { 534 while(!file.eof){ 535 yield parseInt(file.readLine(),10) 536 } 537 }finally { 538 file.close(); 539 } 540 } 541 console.log([...numbers()]); 542 } 543 这断代码在浏览器不会执行,报错。没有readFile. 544 */ 545 546 /* 547 * 2.控制流管理 548 * 如果有一个多步操作非常耗时,采用回调函数,可能会写成: 549 * step1(function(value1){ 550 * step2(function(vlaue2){ 551 * step3(function(value3){ 552 * //Do something with value3 553 * }) 554 * }) 555 * }) 556 * 557 * 采用promise改写上面的代码: 558 * Promise.resolve(step1) 559 * .then(step2) 560 * .then(step3) 561 * .then(function(value3){ 562 * // Do something with value3 563 * },function(error){ 564 * //Handle any error from step1 through step4 565 * }) 566 * 567 * 上面的代码已经把回调函数改写成了直线执行的形式,但是假如了大量的Promise语法。 568 * Generator函数可以进一步改善代码运行流程: 569 * 570 * function * longRunningTask(value1){ 571 * try{ 572 * var value2 = yield step1(value1); 573 * var value3 = yield step2(value2); 574 * var value4 = yield step3(value3); 575 * //Do something with value3 576 * }catch(e){ 577 * //Handle any error from step1 through step4 578 * } 579 * } 580 * 581 * 然后使用一个函数,按次序自动执行所有步骤。 582 * 583 * scheduler(longRunningtask(initialValue)); 584 * function scheduler(){ 585 * var taskObj = task.next(task.value); 586 * if(!taskObj.done){ 587 * task.value = taskObj.value; 588 * scheduler(task) 589 * } 590 * } 591 * 592 * //注意上面这种操作,只适合同步操作,不能有异步操作,因为这里的代码一得到返回值, 593 * //就继续往下执行,没有判断异步操作何时完成。 594 */ 595 596 /* 597 * 3.部署Iterator接口 598 * 利用Generator函数可以在任意对象上部署Iterator接口。 599 */ 600 601 /* 602 * 4.作为数据结构 603 * Generator可以看做数据结构,更确切的是可以看做一个数组结构。因为Generator函数可以返回一系列的值, 604 * 这意味着它可以对任意表达式,提供类似数组的接口。 605 */ 606 </script> 607 </head> 608 <body> 609 </body> 610 </html>