« 回到博客列表

每天一点ES6(16):Generator

Tags: es6, generator

什么是 Generator ?

Generator 函数是 ES6 引入的一种异步编程解决方案,在之前讲IteratorPromise的时候有提到过。

Generator 从字面上理解,叫“生成器”,单看名字感觉和工厂函数有关,似乎是用来创建某些东西的。可以把 Generator 理解为一个状态机,可以在其内部封装多个状态,按序进行产出。执行 Generator 会返回一个遍历器对象,遍历得到的内容就是其内部的各种状态。所以 Generator 实际上是生成了一个可遍历的状态序列。

Generator 的基本语法

Generator 函数跟普通函数没什么两样,但是有两个特征:

一是function关键字与函数名之间有一个星号,具体是更靠近function关键字,还是更靠近函数名,这个没有规定,跟随团队当前所用的代码风格走就好,一般会选择靠近function,因为 Generator 本身还是一个普通函数,只是在定义的时候有所不同。值得注意的一点是,不能使用箭头函数来定义 Generator。

function * gen () {} // 函数定义
const gen = function * () {}  // 函数表达式
const gen = { * gen () {} }   // 函数定义的简写

// 注意,没有这样的写法
// const gen = * () => {}

二是函数体内用yield表达式产出不同的状态,这是 Generator 特有的一个表达式,不能在普通函数中使用。

function* gen () {
  yield 'hello'
  yield 'world'
  return 'end'
}

const hw = gen()

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

如上代码所示,Generator 函数除了定义阶段有些许不同,调用方式和普通函数一样,只不过调用 Generator 并不直接执行函数体的内容,而是返回一个遍历器对象,必须要对这个遍历器对象调用next()才会开始执行函数体,把指针移动到下一个yield表达式,产出其对应的状态,value 就是yield表达式的值,done表示之后是否还有yield表达式。

注意,Generator 不能使用new来创建实例(但可以通过改造来达到同样的效果)

yield 表达式

Generator 中使用 yield 表达式来暂停后续函数的执行,并将紧跟其后的表达式的值作为返回对象的value属性进行产出(yield 字面意思就是“产出”)。下次调用next()时,继续执行,直到下一个yield;或遇到return,那么就把return的值作为最后一个状态的value;或函数运行到最后一行return,那么最后一个状态的value就是undefined

yield后面的表达式只有在调用next()时,内部指针指向该语句,才会执行,这等于是为 JS 提供了手动惰性求值的特性。

Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数,函数在调用时并不会执行函数体里的内容,直到手动调用next()才会执行,并且一次性执行完毕。

yield如果嵌套使用在另一个表达式中,必须放在圆括号里。

function* gen () {
  console.log('Hello' + yield);       // SyntaxError
  console.log('Hello' + yield 123);   // SyntaxError

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

yield表达式单独作为参数或者右值时,可以不加括号

function* gen () {
  foo(yield 'a', yield 'b'); // OK
  let input = yield;         // OK
}

yield 和 return 的区别

yieldreturn很相似,都是返回紧跟在语句后面的表达式的值。区别在于:

与 Iterator 接口的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用这个函数就会返回该对象的一个遍历器对象。Generator 函数就是遍历器生成函数,所以可以直接把 Generator 赋值给Symbol.iterator,以此来部署 Iterator 接口。

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

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

next() 的参数

yield表达式本身并没有返回值,如果尝试执行const foo = yield 1foo的值会是undifined。可以在调用next()时传入一个参数,作为上一个yield的返回值。第一次调用next()时不应该传入参数,因为这一步只是为了启动 Generator,还不存在“上一个 yield”;即便传入了也会被忽略。

利用这一点,可以在 Generator 执行过程中改变其上下文,进而调整其行为。

for...of

for...of循环可以自动遍历 Generator 生成的 Iterator 对象,而不需要手动调用next()方法。for...of会依次获取每次yield表达式的值,直到某一次next()返回的对象的done属性为true时终止,或者遇到了return

配合 Generator,可以用for...of遍历任意对象。一种方法是给对象外面包一层,将对象的键值对以二维数组形式给yield出来。

function* objectEntries(obj) {
  const propKeys = Reflect.ownKeys(obj);

  for (const propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

const jane = { first: 'Jane', last: 'Doe' };

for (const [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

另一种方法是把 Generator 加到对象的Symbol.iterator属性上。

function* objectEntries() {
  const propKeys = Object.keys(this);

  for (const propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

const jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (const [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

除了for...of之外,扩展运算符(...)、解构赋值和Array.from()内部调用的都是遍历器接口。因此 Generator 返回的 Iterator 对象在这些地方都能用。

Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个throw()方法,可以在函数体外抛出错误,在函数体内捕获,但这样的捕获只会发生一次。

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

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

try {
  i.throw('a'); // 第一次抛异常,被内部 catch 捕获
  i.throw('b'); // 第二次抛异常,内部的 catch 已经执行过了,不再捕获, 因此被外部的 catch 捕获。
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

注意这里的throw()throw语句不一样,前者是 Generator 的方法,后者是 JS 的关键字;前者抛出的错误能被 Generator 内部的catch()捕获,后者的只能被外部的catch()捕获;前者可以连续多次调用,依次被从内到外的catch()捕获,后者在被捕获之后,就不再继续执行try里面的代码;调用前者会附带执行一次next(),执行下一条yield表达式,调用后者不带任何附加操作。但两者也有相同之处,两者都建议抛出Error对象,而不是一个随便的原始类型值。

Generator 执行过程中如果抛出了异常,只要被内部捕获,Geneerator 就能继续执行下去。但如果没有被内部捕获,就会终止执行。继续调用next()将返回{value: undefined, done: true}

Generator.prototype.return()

这里的return()类似 JS 里普通的return,返回值并终止函数,这里会返回 value 值为传入参数,done 值为 true 的 Generator 状态对象,并终结遍历 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 }
g.next()        // { value: undefined, done: true }

如果 Generator 函数里有try...finally代码块,那么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,先执行 finally
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true } // return 的结果到这里才出现

本质上,next()throw()return()都是在做同样的事:让 Generator 恢复运行,并用不同的语句替换yield语句。

yield* 表达式

这个特殊的yield表达式用于在一个 Generator 内部调用另一个 Generator。普通的yield只会返回 Generator 对象,但并不会执行它。

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"

yield*后面跟 Generator 函数(没有return),等同于给这个 Generator 内部部署一个for...of循环。实际上任何部署了 Iterator 接口的数据结构都可以yield*进行遍历。

Generator 的this

Generator 函数总是返回一个遍历器,可以通过prototype去定义实例方法,但无法直接通过this去定义。可以通过call()绑定一个空对象作为上下文,把属性都赋值到这个对象上,用这个对象作为 Generator 函数的实例,来变通实现。

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {}
var f = F.call(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

或者更好一点,绑定函数的原型为上下文,这样this直接指向实例本身。

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

小结

Generator 的本质就是一个状态机,相比 ES5 用普通函数实现的效果,不会把内部状态暴露出来,更优雅也更安全。

Generator 也可以看作是 ES6 对协程的不完全实现,多个 Generator 可以以“时间片轮转”的形式实现并行,通过yield控制执行权。

当然,Generator 在实际应用中最广泛的用途,还是用来控制异步流,将其转换为更为直观的类似同步流的写法。

该系列的其他文章

上一篇:每天一点ES6(15):Promise

下一篇:每天一点ES6(17):Async / Await