一、什么是Generator函数?
Generator 函数也是 ES6 提供的一种解决异步编程方案,虽然Generator 函数是一个普通的函数,但是它的语法行为与传统函数完全不同!
1、基本语法
// 基本语法
function* generator() {yield 'hello'yield 'world'return "thanks"
}
const gen = generator()
console.log(gen);
console.log(gen.next()); //next1
console.log(gen.next()); //next2
console.log(gen.next()); //next3
console.log(gen.next()); //next4
你以为这样打印看得到结果就是完了?No!接下来,一步一步分析:
(1)状态
//当只是打印Generator实例时
console.log(gen); //Object [Generator] {}
在浏览器显示会更加详细:
结果显示,[[GeneratorState]]:suspended,即Generator的初始状态是暂停的。什么情况下会改变转改呢?当执行next()时,会发生什么?
console.log(gen.next()); //1
console.log(gen.next()); //2
当继续只执行next1或者执行next1和next2时,Generator实例的状态依旧是[[GeneratorState]]: "suspended",并且,打印的结果分别是:{ value: 'hello', done: false },{ value: 'world', done: false },字段done的值都是false,如图所示:
console.log(gen.next()); //next3
可以看到,Generator实例的状态变更了:[[GeneratorState]]: "closed",代表generator函数执行完毕了,另外,{ value: 'thanks', done: true },字段done的值都是true。执行完毕了还可以继续next嘛?看看就知道,继续执行next4:
console.log(gen.next()); //next4
还是可以,Generator实例的状态依旧是关闭的,只不过,注意观察next4后的结果的{ value: undefined, done: true }。并且,在此之后,无论执行多少个next,结果都是{ value: undefined, done: true }。
(2)执行
Generator 函数generator函数的函数体内部有两个yield表达式yield 'hello'和yield 'world'和一个return语句,表示该函数有三个状态:hello、world和return(结束执行)
第一次调用,Generator 函数generator开始执行,遇到第一个yield则暂停执行,next返回一个对象:{ value: 'hello', done: false },此时遍历还没有结束。
第二次调用,Generator 函数generator从上一次yield暂停执行处继续执行,直到遇到下一个yield则暂停执行,next返回一个对象:{ value: 'world', done: false },此时遍历还没有结束。
第三次调用:Generator 函数generator从上一次yield暂停执行处继续执行,直到遇到return语句(如果没有return语句,就执行到函数结束)则执行完毕,next返回一个对象:{ value: 'thanks', done: true },此时遍历已经结束。
第四次调用:Generator 函数generator函数已经执行完毕,next返回一个对象:{ value: 'undefined', done: true },无论接下来执行N次next,结果一直会是这个,不会再改变。
(3)特征
从这个简单的例子示范,可以看出:
第一:Genenrator函数定义时,function关键字与函数名之间有一个*,一般的写法是紧挨着function关键字;
第二:Genenrator函数的函数体内部可以通过yield表达式标记暂停执行(定义不同的内部状态),即遇到yield表达式,就会暂停执行后面的所有操作,而紧跟在yield后面的值作为返回对象中value属性的值,其实例对象通过next恢复执行,只有调用next方法才会遍历下一个内部状态,已经调用next方法,那么下一个next调用时,会在上次暂停的地方继续执行,而不是从头开始执行。;
第三:Genenrator函数执行时,执行结果会返回一个Object [Generator] {},即遍历器对象。
2、yield表达式&&next方法
相信你已经明白了yield和next的执行逻辑,但是也还有一些细节需要注意:
(1)函数内部没有yield和return语句
Genenrator函数内部没有yield和return语句时,需要使用next方法开启执行,不然函数不会执行,那么函数会直接执行函数内部的代码,该打印就打印,函数内部执行完毕后,next依然会返回一个对象:{ value: undefined, done: true },此时,函数的状态也由[[GeneratorState]]:suspended变成了[[GeneratorState]]:closed。
综上所述,Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
// 没有yield表达式
function* foo() {console.log('hello');console.log('world');
}
const f = foo()
console.log(f.next());
// hello
// world
// { value: undefined, done: true }
(2)函数内部没有return语句时
Genenrator函数内部没有return语句时,则返回的对象的value属性的值为undefined
function* generator() {yield 'hello'
}
const gen = generator()
console.log(gen.next()); //{ value: 'hello', done: false }
console.log(gen.next()); //{ value: undefined, done: true }
(3)yield表达式与return语句相比
yield表达式具备位置记忆的功能,Genenrator函数内部可以执行多个yield表达式,并返回一系列值。其实yield表达式与return语句很相似,都可以返回紧接在后面的值,但是最大的区别就是可以执行多次yield表达式,并且每次执行都会暂停执行,需要next方法的调用来回复执行,同时会从上次暂停的地方继续执行;而return只能执行一次,并且不具备位置记忆功能。
function common() {console.log('开始执行');
}
const foo = common() //开始执行
function* generator() {console.log('开始执行');
}
const gen = generator()
gen.next() //开始执行
(4)yield表达式的使用位置
yield表达式只能在Genenrator函数内部使用,在其他地方使用会报错。这一点与await必须与async配对使用有异曲同工之处。
// yield的使用位置
function testYield1() {yield 'hello'
}
const test = testYield1()
//SyntaxError: Unexpected string 报错
另外,如果有需要将yield表达式用在另外一个表达式之中,则必须放在圆括号()里面,否则会报错。
function* testYield2() {
//错误写法// console.log('Hello ' + yield); // console.log('Hello ' + yield 'World'); //SyntaxError: Unexpected identifier
//正确写法console.log('Hello ' + (yield));console.log('Hello ' + (yield 'World'));
}
const gen = testYield2()
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
// { value: undefined, done: false }
// Hello undefined
// { value: 'World', done: false }
// Hello undefined
// { value: undefined, done: true }
3、中间件next()
在上面的例子中,都只是纯纯地调用next()方法,没有给它传递参数,是不能接收参数嘛?当然不是。next()也可以接收参数,因为yield本身没有返回值。next方法可以携带一个参数,这个参数就会被当做上一个yield表达式的返回值。
function* generator() {for (let i = 1; i < 3; i++) {let yieldVal = yield i;console.log('yieldVal=', yieldVal);if (yieldVal == 2) {i = 0}}
}
let gen = generator()
console.log('nextObj=', gen.next());
console.log('nextObj=', gen.next());
console.log('nextObj=', gen.next(2));
console.log('nextObj=', gen.next());
console.log('nextObj=', gen.next());
//打印结果:
// nextObj= { value: 1, done: false }
// yieldVal= undefined
// nextObj= { value: 2, done: false }
// yieldVal= 2
// nextObj= { value: 1, done: false }
// yieldVal= undefined
// nextObj= { value: 2, done: false }
// yieldVal= undefined
// nextObj= { value: undefined, done: true }
观察yieldVal每次打印的值可以发现,yieldVal=undefined总是等于undefined,即yield总是返回 `当给next传递一个参数2的时候,yieldVal=2,所以符合if条件,i=0,那么下一次循环就会从0开始递增。所以说,next方法的参数会作为外部的值注入到函数体内部,并且被当做上一个yield`表达式的返回值。
简单概括一下:next方法传入的参数会作为上一个yield表达式的返回值,然而,我们在第一次调用next并且传入参数时,是无效的,V8引擎会自动忽略第一使用next的参数,只会从第二次调用next时开始,传入的参数才是有效的。难道第一次调用next没有意义了?当然不是,第一次调用next方法是用来启动遍历器对象的,不需要带参数。
这时候就有一个疑问了,我就是要在第一次调用的时候给next传入参数,并且生效,怎么办?自然还是有解决办法的:
function outerFun(generatorFun) {//参数是一个Generator函数return function() {//return出去一个函数let gen = generatorFun()gen.next()return gen}
}
const outer = outerFun(function*() {console.log(`你第一次传入的参数是: ${yield}`)return 'World'
});
outer().next('Hello!')
//你第一次传入的参数是: Hello~
从上述例子中可以看出,Generator函数作为outerFun(普通函数)的形参,然后return出去一个执行函数(匿名函数),调用outerFun,并传入实参,因为闭包的存在,最后执行匿名函数。可见,Generator函数被函数outerFun包裹了一层,让第一次调用next方法时,传入的参数生效。
4、作为对象的属性
这里肯定让人很奇怪,好歹Generator函数也是函数,函数还可以作为对象的键?是什么限制了我的无知。。。。如果一个对象的属性的Generator函数,该怎么写呢?
let obj = {* generator() {console.log('Genrator函数');return 'hello'}
};
console.log(obj.generator);
//[GeneratorFunction: generator]
通过展示,发现定义generator时,前面多了个*,表示该属性是一个Generator函数,这种写法不便于记忆和理解,也可以写成下面的形式:
let obj = {generator: function*() {console.log('Genrator函数');return 'hello'}
}
console.log(obj.generator); //[GeneratorFunction: generator]
这种形式就是我们熟悉的写法了,函数名字作为对象的键,而函数本身作为对象的值。
5、this指向
Generator函数的this关键字,首先箭头函数是没有this的,那Generator函数里面有this吗?
function Person() {this.name = 'Person'
}
function* Student() {this.name = 'Student'
}
let person = new Person()
console.log(person); //Person { name: 'Person' }
console.log(person.name); //Person
let student = Student()
console.log(student); //Object [Generator] {}
student.next()
console.log(student.name); //undefined
从上述代码可以看出,Generator函数返回的总是遍历器对象,就算在Generator函数内部的this上挂上一个属性name,但是返回的遍历器对象就是拿不到这个属性。难道是Generator函数没有this?有没有,打印以下就知道了:
function* gen() {console.log(this);
}
let g = gen()
g.next()
有打印结果,说明是存在你this的,只不过,Generator函数返回的遍历器对象无法继承this。那怎么样才能访问this呢?遍历器对象不能访问,那正常的对象是不是就可以了呢?
function* gen() {this.name = 'cat'
}
let obj = {}
let g = gen.call(obj)
console.log(obj instanceof gen); //false
console.log(g instanceof gen); //true
console.log(obj); //{name:'cat'}
console.log(g); //Object [Generator] {}
g.next()
console.log(obj.name); //cat
console.log(g.name); //undefined
上述例子中,将Generator函数gen的this绑定到一个空对象上,即Generator函数gen的this指向这个空对象。这样的话,Generator函数gen执行后,就能访问Generator函数gen的this了。不过Generator函数的遍历器对象依旧不能够访问this。那就是要遍历器对象g访问this上的属性,怎么办?有一个办法是将Generator函数的原型替换这个空对象,Generator函数中[[Prototype]]: Generator,Generator函数的prototype是除掉yield以外的语句:
6、与构造函数相比
与构造函数相比,Generator函数无法new。举个例子:
function Person() {}
Person.prototype.say = function() {console.log('good morning!');
}
let person = new Person()
console.log(person instanceof Person);
person.say()
function* Student() {}
Student.prototype.say = function() {console.log('good afternoon!');
}
let student = Student()
console.log(student instanceof Student);
student.say()
//测试能不能使用new操作符
let test = new Student()
console.log(test instanceof Student);
//TypeError: Student is not a constructor
7、与Async-Await相比
为什么说Async-Await是Generator函数的语法糖?那就举个例子,首先定义一个工具函数foo,接收一个形参num:
function foo(num) {return new Promise((resolve, reject) => {setTimeout(() => {resolve(num * 2)}, 1000)})
}
接下来同时分别使用Async-Await和Generator函数来实现:依次调用传递的参数,首先使用Generator函数实现展示:
function* gen() {foo(1)yieldfoo(2)yieldfoo(3)yield
}
let g=gen()
g.next()//2
g.next()//4
g.next()//6
然后看看Async-Await如何实现:
async function AsyncAwait() {
await foo(1)await foo(2)await foo(3)
}
AsyncAwait()
//2 4 6
对比完发下,Async-Await简直不要太~优雅了。就好比声明Generator函数的*换成了async,yield表达式换成了await,但是Async-Await就让人更舒适。
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:




文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取
本文深入探讨Generator函数,包括其基本语法、yield表达式与next方法的使用,中间件next()的应用,以及作为对象属性、this指向、与构造函数和Async-Await的比较。通过实例解析Generator函数在异步编程中的重要作用。
535

被折叠的 条评论
为什么被折叠?



