generator(生成器)
什么是generator函数:
在js中,一个函数一旦执行,就会运行到最后或者遇到return时结束,运行期间不会有其它代码能打断它,也不能从外部再传入值到函数体内。
generator函数的出现就可以打断一个函数的完整运行,语法和传统函数完全不同。
generator是ES6提供的一种异步变成解决方案,形式上也是一个普通函数,但是有几个显著的特征:
1、function关键字和函数名之间有一个星号*,紧挨着function关键字
2、函数体内使用yield表达式,定义不同的内部状态,yield表达式可以有多个
3、直接调用generator函数并不会执行,也不会返回运行结果,而是返回一个遍历器对象(Iterator Object)
4、依次调用遍历器对象的next()方法,遍历generator函数内部的每一个状态
普通函数与generator函数的对比:
function fn(){ return "你好!"; } console.log(fn()) // 你好! ----普通函数一旦调用立即执行 function* fn1(){ yield "你好"; // yield表达式是暂停执行的标记 return "hello"; } var f=fn1(); // 调用generator函数,函数并没有执行,而是返回一个iterator对象 console.log(f.next()) // {value: "你好", done: false} ----value是返回值,done是一个布尔值,false表示遍历还没哟结束 console.log(f.next()) // {value: "hello", done: true} ----done: true表示遍历结束 console.log(f.next()) // {value: undefined, done: true} ----当done为true时,遍历已经结束,不会有返回值,继续下去依然打印这句
generator函数内部的执行像是被人推一下,动一步:
function* go(){ yield "走一步"; yield "再走一步"; return "结束"; } let g=go(); console.log(g.next()); // {value: "走一步", done: false} console.log(g.next()); // {value: "再走一步", done: false} console.log(g.next()); // {value: "结束", done: true} console.log(g.next()); // {value: undefined, done: true}
上面代码中定义了一个generator函数,其中包括两个yield表达式和一个rerun语句,即产生了三个状态。
每次调用iterator对象的next()方法时,内部的指针就会从函数的头部或上一次停下来的地方开始执行,知道遇到下一个yield表达式或return语句暂停。换句话说,generator函数是分段执行的,yield表达式是暂停执行的标记,而next()方法可以恢复执行。
执行过程:
1、第一次调用next()方法时,内部指针从函数头部开始执行,遇到第一个yield表达式时暂停,并返回当前的状态值“走一步”
2、第二次调用next()方法时,内部指针从上一个(这里是第一个)yield表达式开始,遇到第二个yield表达式暂停,返回当前的状态值“再走一步”
3、第三次调用next()方法时,内部指针从第二个yield表达式开始,遇到return语句暂停,返回当前状态的值“结束”。此时所有的状态遍历完毕,done属性的值变为true
4、第四次调用next()方法时,由于函数已经遍历完毕,没有其它状态,因此返回{value:undefined,done:true},如果继续调用next()方法,依然返回这个值
yield表达式:
1、yield表达式只能用在generator函数内,用在其它地方会报错
function fn(){ yield 111; // Uncaught SyntaxError: Unexpected number ----语法错误 } fn();
2、yield表达式如果用在另一个表达式中,必须要放在小括号里,不加小括号会报错
function* demo(){ var str="hello"+(yield 123); var str1="hello"+(yield 456); return str+str1; } var d=demo(); console.log(d.next()) // {value: 123, done: false} console.log(d.next()) // {value: 456, done: false} console.log(d.next()) // {value: "helloundefinedhelloundefined", done: true} console.log(d.next()) // {value: undefined, done: true}
3、yield表达式当做参数放在赋值表达式的右边,可以不加括号
function* demo(){ fn(yield "a",yield "b"); let input=yield; }
4、yield与return的区别:
相似:都能返回紧跟在语句后的那个表达式的值
区别:每次遇到yield,函数就会暂停执行,下一次再从该位置继续向后执行,而return语句不具备记忆位置的功能。一个函数只能有一个return,而在generator函数中可以有多个yield。
yield*表达式:
如果在一个generator函数中调用另一个generator函数,是没有效果的:
function* fn(){ yield "aaa"; yield "bbb"; } function* bar(){ fn(); yield "ccc"; yield "ddd"; } let b=bar(); for(let value of b){ console.log(value) // ccc ddd }
当前只输出bar函数自己的两个状态值,如果想要正确地在bar里调用fn函数,就需要用到yield*表达式。
yield*表达式用于在一个generator函数中调用另一个generator函数:
function* fn(){ yield "aaa"; yield "bbb"; } function* bar(){ yield* fn(); yield "ccc"; yield "ddd"; } let b=bar(); for(let value of b){ console.log(value) // aaa bbb ccc ddd }
next()方法的参数:
yield表达式本身没有返回值,或者说返回值总是一个undefined。next()方法可以带一个参数,这个参数就会被当做是上一个yield表达式的返回值。
[rv] = yield [expression]
expression:定义通过遍历器从生成器函数返回的值,如果省略,则返回undefined
rv:接受从下一个next()方法传递来的参数
例子:
function* fn(){ let num=yield 1+2+3; console.log(num) yield num; } var f=fn(); console.log(f.next()) // {value: 6, done: false} console.log(f.next()) // undefined {value: undefined, done: false} console.log(f.next()) // {value: undefined, done: true}
第一次调用遍历器对象next()方法,函数从头部开始执行,遇到第一个yield暂停,着这个过程中其实是分了三步:
1、声明一个变量num,并将声明提前,默认值为undefined
2、由于generator函数是“惰性求值”,执行到第一个yield时才会计算求和,并将计算结果返回给遍历器对象{value: 6, done: false},函数暂停执行
3、理论上应该把等号右边的yield 1+2+3赋值给变量num,但是,由于函数执行到yield时暂停了,这一步就被挂起了
第二次调用next()方法,函数从上一次yield停下的地方开始执行,也就是给num赋值的地方开始,由于next()并没有传参,就相当于传参为undefined
基于以上分析,不难理解yield表达式本身的返回值([rv])总是undefined。现在当第二次调用next()方法时,传入参数3:
function* fn(){ let num=yield 1+2+3; console.log(num) yield num; } var f=fn(); console.log(f.next()) // {value: 6, done: false} console.log(f.next(3)) // 3 {value: 3, done: false} ----yield表达式的返回值是undefined,当不传参数时,num接收到的是undefined;这里next()带的参数被当成上一个yield表达式的返回值 console.log(f.next()) // {value: undefined, done: true}
如果第一次调用next()的时候也传了一个参数呢?这是无效的,next()方法的参数表示上一个yield表达式的返回值,所以在第一次使用next()方法时,参数无意义,即第一次调用next()方法时,不传参。
generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next()方法的参数,可以在generator函数运行后,继续向函数体内部注入值。也就是说,可以在generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
function* fn(x){ let y=2*(yield (x+1)); let z=yield (y/3); return x+y+z; } let f=fn(5); console.log(f.next()) // {value: 6, done: false} console.log(f.next()) // {value: NaN, done: false} console.log(f.next()) // {value: NaN, done: true} console.log(f.next()) // {value: undefined, done: true}
解析:
1、第一次调用next()方法,函数只会执行到yield (5+1)暂停,并返回{value: 6, done: false}
2、第二次调用next()方法,没有传递参数,所以y的值是undefined,那么y/3是一个NaN,所以返回{value: NaN, done: false}
3、第三次调用next()方法,没有传递参数,所以z的值是undefined,6+undefined+undefined=NaN,返回{value: NaN, done: true}
如果向next()方法提供参数,那么返回结果就完全不一样了:
function* fn(x){ let y = 2*(yield (x+1)); let z = yield (y/3); return x+y+z; } let f=fn(5); console.log(f.next()) // {value: 6, done: false} console.log(f.next(9)) // {value: 6, done: false} console.log(f.next(2)) // {value: 25, done: true} console.log(f.next()) // {value: undefined, done: true}
解析:
1、第一次调用next()方法,小括号里为(5+1),由于小括号内被yield暂停,所以返回 {value: 6, done: false}
2、第二次调用next()方法,上一次是在小括号内暂停的,所以这次从小括号内开始,由于next()传入的值是上一个yield返回的值,所以上一个yield返回的6作废,这次返回的是9,成了let y = 2 * (9),y为18,所以第二次调用next()方法时,返回的是yield (y/3) 也就是18/3,结果为 {value: 6, done: false}
3、第三次调用next()方法,参数2被当做是上一个yield的返回值,也就是yield (y/3)的返回值,即z=2,所以x+y+z=5+18+2=25,返回 {value: 25, done: true}
与iterator接口的关系:
ES6规定,默认的iterator接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterator)。
Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数,执行这个函数,就会返回一个遍历器。
由于执行generator函数实际返回的是一个遍历器,因此可以把generator赋值给对象的Symbol.iterator属性,从而使得该对象具有iterator接口。
let obj={}; function* fn(){ yield 4; yield 5; yield 6; } obj[Symbol.iterator]=fn; // 将generator赋值给对象的Symbol.iterator属性,传统对象没有部署原生iterator接口,不能使用for...of循环和扩展运算符,现在通过给对象添加Symbol.iterator属性和对应的遍历器生成函数,就可以使用了 for(let value of obj){ console.log(value) // 4 5 6 } console.log([...obj]) // [4, 5, 6]
for...of循环:
由于generator函数运行时生成的是一个iterator对象,因此,可以直接使用for...of循环遍历,且此时无需再调用next()方法。
这里需要注意,一旦next()方法返回对象的done属性为true,for...of循环就会终止,且不包含该返回对象。
function* fn() { yield 1; yield 2; yield 3; yield 4; return 5; } for(let item of fn()){ console.log(item) // 1 2 3 4 ----done为true了,for...of循环终止了,返回不了5 }
前面学过fon...in用来遍历对象,for...of用来遍历数组。为什么呢?
原生数组具备iterator接口,即默认部署了Symbol.iterator属性,给对象手动部署Symbol.iterator属性,也可以使用for...of循环。
ES6借鉴C++、java、C#、Python语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。
一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。
for...of循环可以使用的范围包括数组、Set和Map结构、某些类似数字的对象(如argument对象、DOM NodeList对象)、generator对象,字符串。
Generator.prototype.return():
generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值(若没有提供参数,则返回值的value属性为undefined),并终结遍历generator函数。
function* fn() { yield 1; yield 2; yield 3; yield 4; return 5; } var f=fn(); console.log(f.next()) // {value: 1, done: false} console.log(f.next()) // {value: 2, done: false} console.log(f.next()) // {value: 3, done: false} console.log(f.next()) // {value: 4, done: false} console.log(f.next()) // {value: 5, done: true} console.log(f.next()) // {value: undefined, done: true} // 使用return()方法后 function* fn() { yield 1; yield 2; yield 3; yield 4; return 5; } var f=fn(); console.log(f.next()) // {value: 1, done: false} console.log(f.next()) // {value: 2, done: false} console.log(f.return("end")) // {value: "end", done: false} ----如果没有传值,则value值为undefined console.log(f.next()) // {value: undefined, done: false} console.log(f.next()) // {value: undefined, done: true} console.log(f.next()) // {value: undefined, done: true}
generator函数应用举例:
应用一:某公司的年会上有一个抽奖活动,总共6个人可以抽6次,没抽一次,抽奖机会就会递减。
思路:按照常规做法就需要声明一个全局变量来保存剩余的可抽奖次数,而全局变量会造成全局污染,指不定什么时候就被重新赋值了,所以往往不会大家推荐。
let count=6; function draw() { // 执行一段抽奖的逻辑 console.log(`剩余${count}次`) } function startDrawing() { if(count>0){ count--; draw(count); } } let btn=document.createElement("button"); btn.id="start"; btn.textContent="开始抽奖"; document.body.appendChild(btn); document.getElementById("start").addEventListener("click",function () { startDrawing(); }); // addEventListener()的第三个参数默认是false,意思是冒泡阶段执行
事实上,抽奖通常是每个人自己来抽,每抽一次就调用一次抽奖方法,而不是点一次就一次性全部运行完,是可以暂停的,这个不就是generator函数的意义所在吗?
function draw(count) { // 执行一段抽奖的逻辑 console.log(`剩余${count}次`) } function* remain(count) { while (count>0){ count--; yield draw(count); } } let startDrawing=remain(6); let btn=document.createElement("button"); btn.id="start"; btn.textContent="开始抽奖"; document.body.appendChild(btn); document.getElementById("start").addEventListener("click",function () { startDrawing.next(); });
应用二:由于http协议是一种无状态协议,执行一次请求后服务器无法记住是从哪个客户端发起的请求,因此当需要实时把服务器数据更新到客户端时通常采用的方法是长轮询和websocket。这里也可以用generator函数来实现长轮询。
function* ajax() { yield new Promise((resolve,reject)=>{ setTimeout(() => { resolve({code:0}); }, 200); }); } // 长轮询的方法 function update(){ let promise=ajax().next().value; promise.then(res=>{ if(res.code!=0){ setTimeout(() => { console.log("2秒后继续查询……") }, 2000); }else{ console.log(res) } }); } update();
编写一个产生斐波那契数列的函数:
var i=0; function fib(n){ var t, a=0, b=1, arr=[0,1]; while (arr.length<=n){ i++; [a,b]=[b,a+b]; arr.push(b); } // for(let j=n;j>=arr.length;){ // i++; // [a,b]=[b,a+b]; // arr.push(b) // } arr=arr.slice(1); return arr; } console.log(fib(5)) console.log(i) console.log(fib(10)) console.log(i)