ES6 Generator

简单谈谈

Generator:状态机,封装了多个内部状态。执行 Generator 返回一个遍历器对象,用于变量每一次的状态。

简单来说,一个 Generator 函数,就像一段楼梯,规定了楼梯的步数以及没一步的状态,调用它就生成了一段楼梯,每走一步都会有具体的执行内容以及返回值,并且只能往前走( yield ),或是走到头了( return )。

Generator 函数的标志就是 function 后面带 * 号。

一个简单的例子:

function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
}

let hw = helloWorldGenerator();

Generator 函数规定了 3 级台阶(两个 yield 和一个 return ),当函数调用的时候,生成了一段楼梯,接下来就是往上走

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

例子中 helloWorldGenerator 函数生成的 hw (楼梯)一共可以走 3 步,在调用 3next 方法调用结束之后,就会固定返回 { value: undefined, done: true }

返回值

如上代码所示,next 调用后的返回值中的 value 即为 yield 后跟语句的值,done 代表楼梯是否走完,当 Generator 函数碰到 return 语句时,done 即为 true。那要是没有 return 咋办?JavaScript 中每个函数都有默认的返回值:undefined

yield

yieldGenerator 函数内部的一个状态,与 yield 息息相关的是 next 方法。

next 的机制:

  1. 调用 next 方法,函数执行到第一个状态(yield)并暂停,返回 { value: yield 后表达式的值, done: false }
  2. 再次调用,函数恢复执行,直到执行到下一个状态(yield)并暂停,返回 { value: yield 后表达式的值, done: false };若没有遇到下一个状态,则走到 return 返回 { value: return 后表达式的值, done: true }
  3. 接着调用,回到第二步。
  4. 如果再次调用,直接返回 { value: undefined, done: true }

试着想想 JavaScript 中函数的执行过程,在 Generator 函数出现之前,函数都是一股脑的从头执行到末尾,而 Generator 的出现让函数内部实现了一种暂停的效果,可以让函数一步一步的执行。

一些注意点

  1. yield 只能在 Generator 函数中使用。
  2. 即使仅在 Generator 函数中,yeild 也仅仅只能在函数的一级作用域中使用,比如不能再 forEach 中使用。
  3. yeild 表达式如果在另一个表达式中使用,必须放在圆括号中

以下是是一些常见的错误:

// 示例一
(function (){
    yield 1;                                // 错误:不能在非 `Generator` 函数中使用。
})();

// 示例二
let arr = [1, [[2, 3], 4], [5, 6]];

let flat = function* (a) {
    a.forEach(function (item) {
        if (typeof item !== 'number') {
            yield* flat(item);              // 错误:仅能在函数的一级作用域内使用。
        } else {
            yield item;                     // 错误:仅能在函数的一级作用域内使用。
        }
    })
};

// 示例三
function* demo() {
    console.log('Hello' + yield);           // 错误:在一个表达式中使用 yield 必须放在括号中
    console.log('Hello' + yield 123);       // 错误:在一个表达式中使用 yield 必须放在括号中

    console.log('Hello' + (yield));         // 正确
    console.log('Hello' + (yield 123));     // 正确
}

与 Iterator 的关系

通过上面内容,可以很自然的将 Generator 函数与 Iterator 接口挂上关系。Iterator 的定义:

部署在对象的 Symbol.iterator 上,调用该函数会返回该对象的一个遍历器对象,该对象拥有 next 方法。

而调用 Generator 函数,就能给我们返回一个拥有 next 方法的对象。

所以在 Iterator 中我们说的内容,用 Generator 函数我们都可以实现,这里就不过多深入了。

next 方法的参数

yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数就会被当作前个状态(yield)表达式的值。

注意是前一个状态的值!第一次调用 next 方法时,即使传入参数也毫无意义。

其实也很好理解:在调用 next 时,此时 Generator 函数状态时停留在上一个 yield 处的,而 next 调用的参数就相当于给那个状态一个具体的值,当第一次调用 next 时,由于 Generator 函数还么有任何状态,因此也就没用了。

一个简单的例子:

function* foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}

let a = foo(5);
a.next();
// { value: 6, done: false }
a.next();
// { value: NaN, done: false }
// 推到过程:(yield (x + 1)) => undefined => 2 * undefined => NaN => NaN/3 => NaN
a.next();
// { value: NaN, done: true }
// 推到过程:yield (y / 3) => undefined => 5 + NaN + undefined => NaN

let b = foo(5);
b.next();
// { value: 6, done: false }
b.next(12);
// { value: 8, done: false }
// 推到过程:(yield (x + 1)) => 12 => 2 * 12 => 24 => 24/3 => 8
b.next(13);
// { value: 42, done: true }
// 推到过程:yield (y / 3) => 13 => 5 + 24 + 13 => 42

Generator.prototype.throw

Generator 函数返回的遍历器对象,拥有 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

throw 方法可以认为是吧 yield 语句替换成一个 throw 语句。

let g = function* () {
    try {
        yield;
    } catch (e) {
        console.log('内部捕获:', e);
    }
}

let i = g();
i.next();

try {
    i.throw('a');                   // i 处于第一个状态,把第一个 yield 替换为了 throw。
    i.throw('b');                   // i 出于第二个状态,但 g 却并没有实现,因此直接抛错。
} catch (e) {
    console.log('外部捕获:', e)
}
// 内部捕获: a
// 外部捕获: b

**注:**如果 throw 对应的 yield 语句在 Generator 函数内部没有进行 try...catch 的话,是会向外部抛出的,就如例子中的 b

就像程序执行的那样,如果出错没有 try...catch 的话,是会往上抛的。

throw 方法,主要是为了在 Generator 函数外提供一个正常的错误提示机制。

Generator.prototype.return

return 方法作用和 throw 差不多,只是它把 yield 语句换成了 return 语句。

function* gen() {
    yield 1;
    yield 2;
    yield 3;
}

let g = gen();

g.next();
// { value: 1, done: false }
g.return('foo');
// { value: 'foo', done: true }
g.next();
// { value: undefined, done: true }
// 由于上一步已经 return 故没有了下一步。

next throw return 共同点

nextthrowreturn这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。

  • next 是将 yield 表达式替换成一个值。
  • throw 是将 yield 表达式替换成一个 throw 语句。
  • return 是将 yield 表达式替换成一个 return 语句。

yield* 表达式

如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。

function* foo() {
    yield 'a';
    yield 'b';
}

function* bar() {
    yield 'x';
    foo();
    yield 'y';
}

for (let v of bar()){
    console.log(v);
}
// x
// y

就如同例子中的一样,foo 中的楼梯( yield )并没有过加入到 bar 的楼梯上,而正常我们想要的效果应该是两个楼梯合并成一个楼梯,这时候,我们就需要使用到 yield* 表达式,如下所示

function* bar() {
    yield 'x';
    yield* foo();
    yield 'y';
}

// 等同于
function* bar() {
    yield 'x';
    yield 'a';
    yield 'b';
    yield 'y';
}

// 等同于
function* bar() {
    yield 'x';
    for (let v of foo()) {
        yield v;
    }
    yield 'y';
}

for (let v of bar()){
    console.log(v);
}
// 'x'
// 'a'
// 'b'
// 'y'