2

之前断断续续接触到了一些ES6的知识,异步编程方面听得比较多的就是Promise,直到最近比较系统地学习了ES6的新特性才发现Generator这个神奇的存在,它可以实现一些前所未有的事情,让我顿时对它充满了兴趣。

为什么需要Generator?

JavaScript异步编程是为解决JavaScript执行环境是“单线程”这个问题的。在JavaScript中,异步编程的使用非常频繁,也经常会出现需要逐步完成多个异步操作的情况。之前用回调函数实现异步编程如果碰到了这种问题就需要嵌套使用回调函数,异步操作越多,嵌套得就越深,这样非常不利于代码的维护,代码阅读起来也很困难。Generator函数是ES6提出的一种异步编程解决方案,它可以避免回调的嵌套,但是它的用处可不仅仅如此哦,待我细细道来。

举个小例子

function* gen1() {
    yield 1;
    yield 'hello';
    return true;
}
let g1 = gen1();
g1.next();  // Object {value: 1, done: false}
g1.next();  // Object {value: "hello", done: false}
g1.next();  // Object {value: true, done: true}
g1.next();  // Object {value: undefined, done: true}

上面的代码就定义了一个Generator函数,Generator函数的定义跟普通函数差不多,只是在function关键字后面加了一个星号。调用Generator函数后和普通函数不同的是,该函数并不立即执行,也不返回函数执行结果,而是返回一个指向内部状态的generator对象,也可以看作是一个遍历器对象。然后必须调用该对象的next方法,让函数继续走下去,是指针移向下一个状态。每当碰到yield语句,内部指针就停下来,直到下一次调用next()才开始执行。
上面代码调用了四次next方法,遍历才结束。next方法会返回一个有两个属性的对象,value属性的值为当前yield语句的值,done属性的值表示遍历是否结束,即最后一次调用next方法时,再也碰不到yield或者return语句了。
星号写在哪
function关键字和函数名之间的星号写在哪都可以,只要在两者之间即可,但是一般都采取我上面代码的那种写法。

Generator函数本质

上面说了那么多,想必大家已经知道Generator函数是怎么用的了,那么Generator本质上到底是个啥呢?Generator函数的理解有多种:

  1. Generator函数可以被理解成一个状态机,里面封装了多种状态,有兴趣的同学可以去了解一下状态机,操作系统的书里都会讲到。

  2. Generator函数还可以被理解成一个遍历器对象生成器,它返回的遍历器对象可以依次遍历Generator函数内部的每一个状态。这就是为什么之前说Generator函数不仅是为了解决回调函数嵌套问题。Generator函数是生成一个对象,但是调用的时候前面不能加new命令

yield语句

yield语句是Generator函数内部可以暂停执行程序的语句,yield语句后面的值可以是各种数据类型,字符串,整数,布尔值等等都可以。这里主要想说说Generator函数中yield语句和return语句的区别。

和return语句区别

从上面的例子可以看出,函数不仅是碰到yield语句才会停止执行,碰到return语句也会停止执行。这很容易理解,不管怎样Generator函数也是一个函数,碰到return语句必然会停止执行,返回值。那么,两者的区别是什么呢?先来看个例子:

function* gen2() {
    return true;
    yield 1;
    yield 'hello';
}
let g2 = gen2();
g2.next();  // Object {value: true, done: true}
g2.next();  // Object {value: undefined, done: true}

从上面例子可以看出,当碰到return语句时,返回对象的done属性值就为true,遍历结束,不管后面是否还有yield或者return语句。这种区别本质上是因为yield语句具备位置记忆功能而return语句则没有该功能。

再说一点

Generator函数,不管内部有没有yield语句,调用函数时都不会执行任何语句,只有当调用next(),内部语句才会执行,只要调用next(),就会返回一个对象。yield语句只是函数暂停执行的一个标记。

function* gen3() {
    console.log('执行了么?');
}
let g3 = gen3();  // 没有任何输出
g3.next();
// 执行了么?
// Object {value: undefined, done: true}

注意:yield函数不能在普通函数中使用,否则会报错。

next方法

除了yield语句,next方法也是Generator函数实现中很重要的特性。既然next()是一个函数,那么这个函数可以带参数么,当然可以。上面的例子比较简单,都只是一些单纯的yield语句,其实Generator函数和普通函数一样里面是可以进行各种复杂的计算和操作的,也可以有各种循环语句,不仅next方法可以传参数,Generator函数也是可以传参数的,立马上例子:

function* gen4(a) {
    let b = yield (a + 1);
    return b * 2;
}
let g4 = gen4(1);
g4.next();  //  Object {value: 2, done: false}
g4.next();  //  Object {value: NaN, done: true}
let g5 = gen4(1);
g5.next();  //  Object {value: 2, done: false}
g5.next(3);  //  Object {value: 6, done: true}

上面例子中,Generator函数需要接收一个参数a,表面上变量b是用yield语句赋值了,但是遗憾的是这个赋值好像并没有成功,当第二次调用next方法(没有传参数)时,返回的对象value值居然为NaN,而不是我们想的 2 *(1+1)= 4。但是如果第二次调用next方法时,传入一个参数3,返回对象的value值就为6。这可以说明两点:

  1. yield语句没有返回值,或者总是返回undefined;

  2. next方法如果带上一个参数,这个参数就是作为上一个yield语句的返回值。

注意:因为next方法表示上一个yield语句的返回值,所以必须有上一个yield语句的存在,那么第一次调用next方法时就不能传参数。第一个next只是用来启动Generator函数内部的遍历器,传参也没有多大意义。

再说Generator函数与普通函数区别

可以用prototype么?

虽然Generator函数和普通函数区别很大,但是Generator函数的实例也可以继承Generator函数的prototype对象上的方法。

function* gen5() {}
gen5.prototype.say = function() {
    console.log('有generator?');
}
let g6 = gen5();
g6.say();  // 有generator?

从上面代码可以看出,Generator函数返回的g6,继承了gen5.prototype。

this咋用?

大家都知道普通函数都会有一个this对象,那么Generator的this对象怎么用呢?还是例子更直观:

function* gen6() {
    this.a = 1;
}
let g7 = gen6();
g7.a;  //  undefined

上面代码中,Generator函数在this对象上添加了一个属性a,g7实例并不能取到这个属性。那么怎么让Generator函数返回一个可以正常使用this对象的实例呢?阮一峰老师提供了一种方法,首先,生成一个空对象,使用call方法绑定Generator函数内部的this。这样,构造函数调用以后,这个空对象就是Generator函数的实例对象了。参考代码在这:http://es6.ruanyifeng.com/#docs/generator

Generator函数与Iterator

Generator函数返回的是一个遍历器对象,那么它在遍历这方面肯定有用武之地,下一次讨论Iterator时候再总结吧。


hieeyh
963 声望148 粉丝

工科女,爱运动,爱思考,爱接触新鲜事物