ES6学习笔记(18)之 Generator 函数的语法

参考:ECMAScript 6 入门

Generator 函数概览

调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针(类比Iterator)。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值(yield本来就是产出的意思,它代表后面的计算值就是它的产出值);done属性是一个布尔值,表示是否遍历结束。
举例:

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

var hw = helloWorldGenerator(); // 生成一个生成器对象(好比生成一个遍历器对象)

hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }

为什么 Generator 函数叫 Generator

因为 Generator 函数里用了yield啊,yield就是产出的意思,所以它叫生成器函数。

yield用法的注意事项(语法层面的东西我认为是非重点,如果开发过程中语法报错,查文档就可以了):

  • yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}
  • yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

Generator 与 Iterator 的关系:

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]

我的问题:既然 Generator 实现的就是遍历功能,只用 Iterator 就行了,为什么还多此一举再弄一个 Generator?

理解yield的返回值:

next方法返回的是yield关键字后面表达式的计算值,但是yield和它后面的表达式组合起来的表达式没有返回值,或返回值是undefined,如何理解?
第一种情况举例:

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

var hw = helloWorldGenerator(); // 生成一个生成器对象(好比生成一个遍历器对象)

hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }

第二种情况举例:

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

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

我们预想的结果是:y为12,z为4,return值为(5+12+4),但实际结果是:y为NaN,z为NaN,return值为(5+NaN+NaN),所以我们得出结论,在var y = 2 * (yield (x + 1));这个语句中,(yield (x + 1))这个表达式没有返回值。

那如何让类似(yield (x + 1))的表达式有返回值呢?
答案是通过next方法值,请看例子:

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

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

在上述例子中,b.next(12)中的12定义了(yield (x + 1))的返回值。也就是说,next方法里传的参数定义了上一个yield表达式的返回值(隐藏了一个信息就是,调用的第一个next方法不用传参数,即使传了也没用,因为它没有上一个yield表达式)。所以在上例中,x为5,y为2*12,z为13,return值为42。

含有yield表达式的语句是如何运行的?

Screen Shot 2019-09-11 at 3.27.43 PM.png

我本来以为第一次执行next方法的结果是:Started, 1. undefined;同理,第二次执行next方法是:2. undefined
但是,就我们看到的结果,很明显执行next方法的时候,它不会执行它所在的语句,只会返回yield后面表达式的计算值,另外从上一个包含yield的语句开始执行。

for...of 循环:

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

两点:

  1. 为什么打印结果没有6?

一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

  1. 为什么for循环里这样写let v of foo()?
    因为我们要从遍历器对象里遍历,foo()返回一个遍历器对象;
    那为什么平时我们遍历数组等时,没见调用方法?因为它默认隐式调用了。
    无论解构还是什么,都是操作遍历器对象的。比如:
let array = [1,2,3];
console.log(...array); // array默认调用了生成遍历器对象的方法
function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

// 扩展运算符
[...numbers()] // [1, 2],必须要numbers(),因为它才是遍历器对象

Generator.prototype.throw() 知识点太琐碎

内部捕获错误这种机制的好处:

这种函数体内捕获错误的机制,大大方便了对错误的处理。多个yield表达式,可以只用一个try...catch代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次catch语句就可以了。

我对上面这段话的理解是:
一般的做法,如果我们要对抛出的每个错误都处理,我们要这样做:

try {
  throw new Error('error 1');
} catch (e) {
  console.log('error 1:', e);
}

try {
  throw new Error('error 2');
} catch (e) {
  console.log('error 2:', e);
}

如果写成如下这样我们如下:

try {
  throw new Error('error 1');
  throw new Error('error 2');
} catch (e) {
  console.log(e);
}

当捕获到error 1的错误后,就不再执行了。而且也没有机会执行throw new Error('error 2');这句。

使用Generator.prototype.throw()就可以完美的将两种优点结合起来:

try {
  g.throw('error 1'); // 这个错误将会在 Generator 函数内部处理
  g.throw('error 2'); // 这个错误将会交由下面的catch处理
} catch (e) {
  console.log(e); // 打印 error 2
}

遍历器对象抛出的错误优先被 Generator 函数内定义的catch捕获处理,如果还有多余的错误处理不了了,遍历器对象将错误抛到外层。如果遍历器对象抛出的错误内外层都处理不了,那么程序将报错,直接中断执行。

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

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

try {
  i.throw('a'); // 优先被内部的catch处理
  i.throw('b'); // 内部的catch已经执行过一次,已无法处理错误,抛出到外层,交由外层处理。
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。

function* gen() {
  try {
    yield 1;
  } catch (e) {
    console.log('内部捕获');
  }
}

var g = gen();
g.throw(1); //没执行过next方法,Generator 函数还没有开始执行,这时throw方法抛错只可能抛出在函数外部。
// Uncaught 1

throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。

var gen = function* gen(){
  try {
    yield console.log('a');
  } catch (e) {
    // ...
  }
  yield console.log('b');
  yield console.log('c');
}

var g = gen();
g.next() // a;因为yield的暂停功能,执行完这个next方法后,程序还停留在try代码块中,所以下面的throw才可以被catch
g.throw() // b; 附带执行一次next方法
g.next() // c

一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。(即使错误被外部捕获,next方法也不会再执行下去了)

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}

function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next();
    console.log('第一次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  try {
    v = generator.next(); // 执行到这里时,会执行throw new Error('generator broke!')这句,抛出的这个错误又没被内部捕获,再执行next方法,Generator 函数就已经结束了
    console.log('第二次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  try {
    v = generator.next();
    console.log('第三次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  console.log('caller done');
}

log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done

Generator.prototype.return()

作用:
可以返回给定的值,并且终结遍历 Generator 函数。
举例:

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

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true } 到这里就结束执行了,并返回了foo
g.next()        // { value: undefined, done: true }

有一种情况,即使return了还会执行,那就是有finally的情况:

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }

g.return(7)
// { value: 4, done: false },因为还有finally要执行,所以运行不会结束,
// 它会当做执行一次next方法,并设置了运行结束后的返回值

g.next() // { value: 5, done: false }

g.next()
// { value: 7, done: true },finally代码块执行结束后,运行结束了,所以yield 6不会执行。
// 因为设置了最终返回值是7,所以执行这个next返回7

让我们来总结下都有哪些情况可以结束运行:

  • 正常运行next到最后一个yield
  • 遍历器对象抛出的错误没有被内部捕获
  • 遍历器对象调用return方法,但finally会延迟结束情况的发生

next()、throw()、return() 的共同点

next()、throw()、return()这三个方法本质上是同一件事,它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

  • next(1)方法就相当于将yield表达式替换成一个值1。如果next方法没有参数,就相当于替换成undefined。
  • throw()是将yield表达式替换成一个throw语句。
  • return()是将yield表达式替换成一个return语句。

如何在 Generator 函数中再调用 Generator 函数

关键词:yield*

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

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"
Screen Shot 2019-09-16 at 11.12.42 AM.png

带 return 的 Generator 函数

单独的:


Screen Shot 2019-09-16 at 11.20.35 AM.png

Generator 函数中调用带 return 的 Generator 函数:

Screen Shot 2019-09-16 at 11.27.29 AM.png

解释:如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。上面代码在第四次调用next方法的时候,屏幕上会有输出,这是因为函数foo的return语句,向函数bar提供了返回值。

使用 Generator 函数进行递归

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]); //在些递归
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

[...iterTree(tree)] // ["a", "b", "c", "d", "e"]

Generator 函数作为对象属性如何定义?

一般的:

let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

简写的:

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

Generator 函数的this(要重点注意)

  • Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。而Generator 函数里面的this并不指的是遍历器对象。
function* g() {
  this.a = 11;
}

g.prototype.hello = function () {
  return 'hi!';
};

let obj = g();
obj.next();
obj instanceof g // true
obj.hello() // 'hi!'
obj.a // undefined;说明里面的this指向的不是遍历器对象
  • 如何使用 Generator 函数里面的this?
    答:Generator 函数本质上是加了语法糖的函数,是函数就可以通过改变执行上下文的方法改变里面的this指向(call或apply)
function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj); // 使里面的this指向obj

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3
  • 如果我想在遍历器对象上访问属性呢?基于上一个的方法,再加上遍历器对象可以访问它的原型对象,那我们就把它的原型对象传进去。
function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3
  • 如果我想使用new呢?那就再上一步的基础上再包一层
function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

// 用该方法再包一层
function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

你可能感兴趣的:(ES6学习笔记(18)之 Generator 函数的语法)