希沃ENOW大前端
公司官网:CVTE(广州视源股份)
团队:CVTE旗下未来教育希沃软件平台中心enow团队
本文作者:
前言
遥想当初面试的时候,被问到经典问题ES6都有啥,年纪轻轻的我搜刮了满脑子,除了水就是水。
那么ES6都有啥呢,很快脑子里就有let和const、模板字符串、解构赋值、展开运算符、箭头函数等等。说了这些也许足够让面试官展开发挥了,但是优秀的掘金er怎么可能止步于此?
为了让面试官眼前一亮,es6还有这些新特性:模块化、Symbol、Set和Map数据结构、Proxy代理和Reflect反射、Generator等等,说完面试官估计眼睛开始有光了,那么说了就要懂,懂了才不怕被面试官问,虽然有点冷门,俗话说技多不压身,今天就一起来看看Generator吧!
generator是什么
generator即生成器,是ES6规范带来的新内容,在generator能够让我们在函数执行时任意地方暂停,在后续遇到合适的时机需要使用这个函数时继续执行。以往我们遇到的函数都是一口气执行到底,而generator的特点就是让函数执行到中间“刹车”,在需要它的时候接着执行。下面从一个案例来看看generator的基本用法吧!
常见的javascript函数:
function fun() {
console.log(1)
console.log(2)
console.log(3)
}
function run() {
console.log(4)
}
fun()
run()
// 结果:
1
2
3
4
使用generator函数:
function* funG() {
yield console.log(1)
yield console.log(2)
yield console.log(3)
}
function run() {
console.log(4)
}
const iter = funG()
iter.next()
run()
iter.next()
iter.next()
iter.next()
// 结果:
1
4
2
3
{value: undefined, done:true}
写法上:
- generator相对于普通函数在function后面多加了*号。
- 在每个我们需要中断执行的语句前加了yield,通过yield来控制函数执行。
从打印结果上来看:
- 普通函数一口气打印了1,2,3,4。
- generator打印结果明显不同,当调用generator函数的时候并不是立即执行,返回的是一个生成器内部指针对象iter,通过调用.next()方法移动指针对象到下一个yield,执行表达式,返回表达式结果并暂停自身。每执行一次,都会返回一个包含value和done属性的对象,value为当前表达式的值,done是boolean值,当done的值为true的时候,表示生成器执行完成。
知道generator的基本用法了,但还不够,为了加深面试官的印象,我们可以从generator设计出发点聊聊协程!
generator与协程
既然要聊协程,首先得知道协程是什么吧!
简单来说协程就像单身程序员小王敲代码,老大给了他一个项目A,小王收到立马开码;
小王项目A做到一半,老大说有个项目B时间赶,赶紧来干项目B;
于是小王停止开发项目A,着手开干项目B;
项目B开发一段时间后,小王回来接着干项目A。
这就是协程,那么项目B做完了?也许没有。
看完了协程的案例,聪明的你应该想到了协程跟generator之间的关系!没错,generator就是协程在js上的实现。通过generator,我们可以在单线程的JavaScript里使用协程!
generator的特性用法
generator本身作为异步编程的解决方案,可以用来解决异步任务。除此之外还可以有更灵活的用法!那便是在函数执行过程中传入参数,以及获取该段表达式输出结果!
上文提到generator每次执行next()方法都会返回一个对象:{ value, done },通过value,我们可以获取该段表达式的返回结果,另外,next还可以接受参数,利用这一特性,我们可以随时传入参数!
function* run(name) {
let who = yield name + ' Allen';
return who
}
let flashMan = run('Barry')
flashMan.next() // { value: 'Barry Allen', done: false }
// 传入参数
flashMan.next('Arrow') // { value: 'Allow', done: true }
在第一个next()调用时,不传入参数,默认将name的参数值'Barry'与' Allen'组合字符串,这个值被yield返回。
在第二个next()调用时,传入参数'Arrow',这个值被变量who接收,因此返回value属性的值为'Arrow',也就是who的值。
不仅如此,generator还可以在函数运行时,捕获函数体外抛出的错误。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('error');
// error
generator的简单实现
generator的原理是转化为switch-case来实现的,先从一个简单的案例入手。
function* funG() {
yield 1
yield 2
yield 3
}
const iter = funG()
iter.next() // {value: 1, done: false}
iter.next() // {value: 2, done: false}
iter.next() // {value: 3, done: false}
iter.next() // {value: undefined, done: true}
generator的实现需要一个函数,这个函数可以多次调用,每次返回一个结果,还可以传入参数,那么switch-case就是很好的选择。
function funGen(count) {
switch(count) {
case 1:
return {value: 1, done: false};
case 2:
return {value: 2, done: false};
case 3:
return {value: 3, done: false};
case 'done':
return {value: undefined, done: true}
}
}
funGen(1) // {value: 1, done: false};
funGen(2) // {value: 2, done: false};
funGen(3) // {value: 3, done: false};
funGen('done') // {value: undefined, done: true}
从结果上来看是我们想要的结果,但是距离generator还有很大的差距,接下来创建一个函数,这个函数返回一个对象,通过调用这个对象来帮我们执行14~17行的语句。
function funGen(count) {
switch(count) {
case 1:
return {value: 1, done: false};
case 2:
return {value: 2, done: false};
case 3:
return {value: 3, done: false};
case 'done':
return {value: undefined, done: true}
}
}
const gen = function() {
let count = 0
return {
next: function() {
++count
count = count > 3 ? 'done' : count
return funGen(count)
}
}
}
const test = gen()
test.next() // {value: 1, done: false}
test.next() // {value: 2, done: false}
test.next() // {value: 3, done: false}
test.next() // {value: undefined, done: true}
到目前为止,都需要我们手动处理函数上下文里的count的值的变化来决定返回结果,所以我们需要一个对象来保存函数上下文也就是count的值。
function example(context) {
while(1) {
context.pre = context.next
switch(context.pre) {
case 1:
context.next = 2
return 1;
case 2:
context.next = 3
return 2;
case 3:
context.next = 'done'
return 3;
case 'done':
return context.end()
}
}
}
const gen = function() {
return {
next: function() {
value = context.done ? undefined : funGen(context)
done = context.done
return {
value,
done
}
}
}
}
const context = {
pre: 1,
next: 1,
done: false,
end: function end() {
this.done = true
}
}
const test = gen()
test.next() // {value: 1, done: false}
test.next() // {value: 2, done: false}
test.next() // {value: 3, done: false}
test.next() // {value: undefined, done: false}
通过一个新对象context来记录函数上下文的初始状态pre,以及下一个状态next,done记录是否到最终点。而end便是修改done为结束状态true。
funGen每次运行next()结束后都会将运行状态保存到context中,方便下次运行时获取。
总结
gernerator作为es6的新特性,在后来被更方便好用的async await代替,但是generator独特的特性可以让我们在函数执行的过程中传递参数获取结果,使得函数调用变得更加灵活。作为一个开发者,我们有必要了解一下gernerator的基本使用以及简单的实现的原理,方便在特殊的场景中解决问题。