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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容