[转]JavaScript基础:生成器

ES6 的生成器特性,其他语言里边大多都有这个特性,可用于实现协程,但个人感觉大多数写异步逻辑的场景还是用 Promise 方便,原文链接:juejin.cn/post/6928660813683097613 ,作者:菩提谛听

概述#

JavaScript 中函数一旦启动运行,在它结束之前是不会被中断的。但在 ES6 中引入了一个全新的函数形式生成器 Generator。这种函数不像普通函数一样保证执行运行到结束,而是具有函数块内暂停和恢复代码执行的能力。生成器 Generator 的形式只是在普通函数名称之前加一个 * 表示这是一个生成器。其特征主要有两个:1、function 关键字与函数名之间有一个星号;2、函数体内部使用关键字 yield 表达式,定义不同的内部状态。生成器 Generator 的使用与普通函数一样,只是调用生成器函数会产生一个迭代器,因此可以调用迭代器的 next 函数让函数开始或恢复执行。

// 这是一个 Generator

function *genFunc () {
  yield 'generator';
  yield 'hello';
  yield 'word';
  return '!';
}

const it = genFunc();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());

如下其执行结果就是迭代器产生的内容:

gen的log结果

yield 关键字#

生成器通过使用 yield 关键字便是暂停点。也就是说,生成器函数遇到 yield 关键字之前会正常执行,遇到该关键字时,执行会暂停函数的作用域状态会被保留下来,直到调用 next() 函数恢复执行。此时的 yield 关键字有点像是函数的中间返回语句,它生成的值会出现在 next() 返回的对象里。yield 关键字只能用在生成器函数内部,其它地方会抛出错误。

// 普通函数使用 yield

function func () {
  yield '测试 yield关键字的使用';
}

报错

yield* 表达式#

如何在生成器函数中调用另一个生成器函数,理论上需要在函数体内手动遍历执行另一个生成器函数。

function *gen1 () {
  yield 'gen1: hellow';
  yield 'gen1: word';
  return 'gen1';
}

function *gen2 () {
  for (const genValue of gen1()) {
    console.log(genValue);
  }
  yield 'gen2: hellow';
  yield 'gen2: word';
  return 'gen2';
}

const it = gen2();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());

手动迭代的结果

ES6 提供了 yield* 表达式即是 yield 委托(yield delegation)解决此类问题。yield* ... 需要一个 iterable 对象,然后它会调用 iterable 的迭代器,把自己的生成器控制委托到这个迭代器,直到其耗尽为止。

function *gen3 () {
  const genValue = yield* gen1();
  console.log('genValue:', genValue);
  yield 'gen3: hellow';
  yield 'gen3: word';
  return 'gen3';
}

const it = gen3();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());

yield*的结果 因此可以使用 yield* 表达式实现递归操作。

function *genMult (x) {
  if (x < 3) {
      x = yield* genMult(x + 1);
  }
  return x * 2;
}

const it = genMult(1);
it.next(); // {value: 24, done: true}

yield 输入和输出#

yield 既可以作为函数的中间返回语句使用,也可以作为函数的中间参数使用。yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。

function *sum (x) {
  const y = yield;
  const z = yield;
  return x + y + z;
}

const it = sum(1);
console.log(it.next());
console.log(it.next(2));
console.log(it.next(3));

yield输入 其中第一次调用 next 出入的值不会被使用,因为第一次调用是为了开始执行生成器函数。

提前终止生成器#

与迭代器类似可以使用 returnthrow 提前结束跌倒器的状态,生成器也是可以使用 returnthrow 方法提前终止生成器。return 方法会强制生成器进入关闭状态,return 方法传入的值,就是终止生成器的值。切后续关闭状态无法恢复,后续再次调用 next 方法 done: true 会一直保持。

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

const it = returnFunc();
console.log(it.next());
console.log(it.return(4));
console.log(it.next());

return的log return 方法除了关闭生成器之外,如何生成器函数中包含 finally 子句,还可以执行情路任务如何资源释放、状态重置等。也就是说如果生成器函数内部有 try...finally 代码块,且正在执行 try 代码块,那么 return() 方法会导致立刻进入 finally 代码块,执行完以后,整个函数才会结束。

function *returnFunc () {
  try {
    yield 1;
    yield 2;
  } finally {
    yield 3;
    yield 5;
  }
}

const it = returnFunc();
console.log(it.next());
console.log(it.return(4));
console.log(it.next());
console.log(it.next());

finally的值 注意调用 return() 方法后,就立即执行 finally 代码块,不执行 try 里余下的代码了,然后等到 finally 代码块执行完,再返回 return() 方法指定的值。

throw 方法会在暂停的时候将一个提供的错误注入到生成器对象中,相当于在暂停点出入一个 throw ...。如果生成器内部有 catch 模块对错误进行处理,那么生成器就不会关闭,而且还可以恢复执行。

function *throwFunc () {
  try {
    yield 1;
    yield 2;
  } catch (e) {
      console.error(e);
    yield 3;
    yield 5;
  }
}

const it = throwFunc();
console.log(it.next());
console.log(it.throw(new Error('throw error')));
console.log(it.next());

throw的log结果