博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[译] 什么是 JavaScript 生成器?如何使用生成器?
阅读量:6330 次
发布时间:2019-06-22

本文共 7599 字,大约阅读时间需要 25 分钟。

  • 原文地址:
  • 原文作者:
  • 译文出自:
  • 本文永久链接:
  • 译者:
  • 校对者:,

在本文中,我们将了解 ECMAScript 6 中引入的生成器(Generator)。先看一看它究竟是什么,然后用几个示例来说明它的用法。

什么是 JavaScript 生成器?

生成器是一种可以用来控制迭代器(iterator)的函数,它可以随时暂停,并可以在任意时候恢复。

上面的描述没法说明什么,让我们来看一些例子,解释什么是生成器,以及生成器与 for 循环之类的迭代器有什么区别。

下面是一个 for 循环的例子,它会在执行后立刻返回一些值。这段代码其实就是简单地生成了 0-5 这些数字。

for (let i = 0; i < 5; i += 1) {  console.log(i);}// 它将会立刻返回 0 -> 1 -> 2 -> 3 -> 4复制代码

现在看看生成器函数。

function * generatorForLoop(num) {  for (let i = 0; i < num; i += 1) {    yield console.log(i);  }}const genForLoop = generatorForLoop(5);genForLoop.next(); // 首先 console.log - 0genForLoop.next(); // 1genForLoop.next(); // 2genForLoop.next(); // 3genForLoop.next(); // 4复制代码

它做了什么?它实际上只是对上面例子中的 for 循环做了一点改动,但产生了很大的变化。这种变化是由生成器最重要的特性造成的 —— 只有在需要的时候它才会产生下一个值,而不会一次性产生所有的值。在某些情景下,这种特性十分方便。

生成器语法

如何定义一个生成器函数呢?下面列出了各种可行的定义方法,不过万变不离其宗的是在函数关键词后加上一个星号。

function * generator () {}function* generator () {}function *generator () {}let generator = function * () {}let generator = function* () {}let generator = function *() {}let generator = *() => {} // SyntaxErrorlet generator = ()* => {} // SyntaxErrorlet generator = (*) => {} // SyntaxError复制代码

如上面的例子所示,我们并不能使用箭头函数来创建一个生成器。

下面将生成器作为方法(method)来创建。定义方法与定义函数的方式是一样的。

class MyClass {  *generator() {}  * generator() {}}const obj = {  *generator() {}  * generator() {}}复制代码

yield

现在让我们一起看看新的关键词 yield。它有些类似 return,但又不完全相同。return 会在完成函数调用后简单地将值返回,在 return 语句之后你无法进行任何操作。

function withReturn(a) {  let b = 5;  return a + b;  b = 6; // 不可能重新定义 b 了  return a * b; // 这儿新的值没可能返回了}withReturn(6); // 11withReturn(6); // 11复制代码

yield 的工作方式却不同。

function * withYield(a) {  let b = 5;  yield a + b;  b = 6; // 在第一次调用后仍可以重新定义变量  yield a * b;}const calcSix = withYield(6);calcSix.next().value; // 11calcSix.next().value; // 36复制代码

yield 返回的值只会返回一次,当你再次调用同一个函数的时候,它会执行至下一个 yield 语句处(译者注:前面的 yield 不再返回东西了)。

在生成器中,我们通常会在输出时得到一个对象。这个对象有两个属性:valuedone。如你所想,value 为返回值,done 则会显示生成器是否完成了它的工作。

function * generator() {  yield 5;}const gen = generator();gen.next(); // {value: 5, done: false}gen.next(); // {value: undefined, done: true}gen.next(); // {value: undefined, done: true} - 之后的任何调用都会返回相同的结果复制代码

在生成器中,不仅可以使用 yield,也可以使用 return 来返回同样的对象。但是,在函数执行到第一个 return 语句的时候,生成器将结束它的工作。

function * generator() {  yield 1;  return 2;  yield 3; // 到不了这个 yield 了}const gen = generator();gen.next(); // {value: 1, done: false}gen.next(); // {value: 2, done: true}gen.next(); // {value: undefined, done: true}复制代码

yield 委托迭代

带星号的 yield 可以将它的工作委托给另一个生成器。通过这种方式,你就能将多个生成器连接在一起。

function * anotherGenerator(i) {  yield i + 1;  yield i + 2;  yield i + 3;}function * generator(i) {  yield* anotherGenerator(i);}var gen = generator(1);gen.next().value; // 2gen.next().value; // 3gen.next().value; // 4复制代码

在开始下一节前,我们先观察一个第一眼看上去比较奇特的行为。

下面是正常的代码,不会报出任何错误,这表明 yield 可以在 next() 方法调用后返回传递的值:

function * generator(arr) {  for (const i in arr) {    yield i;    yield yield;    yield(yield);  }}const gen = generator([0,1]);gen.next(); // {value: "0", done: false}gen.next('A'); // {value: undefined, done: false}gen.next('A'); // {value: "A", done: false}gen.next('A'); // {value: undefined, done: false}gen.next('A'); // {value: "A", done: false}gen.next(); // {value: "1", done: false}gen.next('B'); // {value: undefined, done: false}gen.next('B'); // {value: "B", done: false}gen.next('B'); // {value: undefined, done: false}gen.next('B'); // {value: "B", done: false}gen.next(); // {value: undefined, done: true}复制代码

在这个例子中,你可以看到 yield 默认是 undefined,但如果我们在调用 yield 时传递了任何值,它就会返回我们传入的值。我们将很快利用这个特性。

初始化与方法

生成器是可以被复用的,但是你需要对它们进行初始化。还好初始化的方法十分简单。

function * generator(arg = 'Nothing') {  yield arg;}const gen0 = generator(); // OKconst gen1 = generator('Hello'); // OKconst gen2 = new generator(); // 不 OKgenerator().next(); // 可以运行,但每次都会从头开始运行复制代码

如上所示,gen0gen1 不会互相影响,gen2 完全不会运行(会报错)。因此初始化对于保证程序流程的状态是十分重要的。

下面让我们一起看看生成器给我们提供的方法。

next() 方法

function * generator() {  yield 1;  yield 2;  yield 3;}const gen = generator();gen.next(); // {value: 1, done: false}gen.next(); // {value: 2, done: false}gen.next(); // {value: 3, done: false}gen.next(); // {value: undefined, done: true} 之后所有的 next 调用都会返回同样的输出复制代码

这是最常用的方法。它每次被调用时都会返回下一个对象。在生成器工作结束时,next() 会将 done 属性设为 truevalue 属性设为 undefined

我们不仅可以用 next() 来迭代生成器,还可以用 for of 循环来一次得到生成器所有的值(而不是对象)。

function * generator(arr) {  for (const el in arr)    yield el;}const gen = generator([0, 1, 2]);for (const g of gen) {  console.log(g); // 0 -> 1 -> 2}gen.next(); // {value: undefined, done: true}复制代码

但它不适用于 for in 循环,并且不能直接用数字下标来访问属性:generator[0] = undefined

return() 方法

function * generator() {  yield 1;  yield 2;  yield 3;}const gen = generator();gen.return(); // {value: undefined, done: true}gen.return('Heeyyaa'); // {value: "Heeyyaa", done: true}gen.next(); // {value: undefined, done: true} - 在 return() 之后的所有 next() 调用都会返回相同的输出复制代码

return() 将会忽略生成器中的任何代码。它会根据传值设定 value,并将 done 设为 true。任何在 return() 之后进行的 next() 调用都会返回 done 属性为 true 的对象。

throw() 方法

function * generator() {  yield 1;  yield 2;  yield 3;}const gen = generator();gen.throw('Something bad'); // 会报错 Error Uncaught Something badgen.next(); // {value: undefined, done: true}复制代码

throw() 做的事非常简单 —— 就是抛出错误。我们可以用 try-catch 来处理。

自定义方法的实现

由于我们无法直接访问 Generator 的 constructor,因此如何增加新的方法需要另外说明。下面是我的方法,你也可以用不同的方式实现:

function * generator() {  yield 1;}generator.prototype.__proto__; // Generator {constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, Symbol(Symbol.toStringTag): "Generator"}// 由于 Generator 不是一个全局变量,因此我们只能这么写:generator.prototype.__proto__.math = function(e = 0) {  return e * Math.PI;}generator.prototype.__proto__; // Generator {math: ƒ, constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, …}const gen = generator();gen.math(1); // 3.141592653589793复制代码

生成器的用途

在前面,我们用了已知迭代次数的生成器。但如果我们不知道要迭代多少次会怎么样呢?为了解决这个问题,需要在生成器函数中创建一个无限循环。下面以一个会返回随机数的函数为例进行演示:

function * randomFrom(...arr) {  while (true)    yield arr[Math.floor(Math.random() * arr.length)];}const getRandom = randomFrom(1, 2, 5, 9, 4);getRandom.next().value; // 返回随机的一个数复制代码

这是个简单的例子。下面来举一些更复杂的函数为例,我们要写一个节流(throttle)函数。如果你还不知道节流函数是什么,请参阅。

function * throttle(func, time) {  let timerID = null;  function throttled(arg) {    clearTimeout(timerID);    timerID = setTimeout(func.bind(window, arg), time);  }  while (true)    throttled(yield);}const thr = throttle(console.log, 1000);thr.next(); // {value: undefined, done: false}thr.next('hello'); // 返回 {value: undefined, done: false} ,然后 1 秒后输出 'hello'复制代码

还有没有更好的利用生成器的例子呢?如果你了解递归,那你肯定听过。通常我们是用递归来解决这个问题的,但有了生成器后,可以这样写:

function * fibonacci(seed1, seed2) {  while (true) {    yield (() => {      seed2 = seed2 + seed1;      seed1 = seed2 - seed1;      return seed2;    })();  }}const fib = fibonacci(0, 1);fib.next(); // {value: 1, done: false}fib.next(); // {value: 2, done: false}fib.next(); // {value: 3, done: false}fib.next(); // {value: 5, done: false}fib.next(); // {value: 8, done: false}复制代码

不再需要递归了!我们可以在需要的时候获得数列中的下一个数字。

将生成器用在 HTML 上

既然是讨论 JavaScript,那显然要用生成器来操作下 HTML。

假设有一些 HTML 块需要处理,可以使用生成器来轻松实现。(当然除了生成器之外还有很多方法可以做到)

我们只需要少许代码就能完成此需求。

const strings = document.querySelectorAll('.string');const btn = document.querySelector('#btn');const className = 'darker';function * addClassToEach(elements, className) {  for (const el of Array.from(elements))    yield el.classList.add(className);}const addClassToStrings = addClassToEach(strings, className);btn.addEventListener('click', (el) => {  if (addClassToStrings.next().done)    el.target.classList.add(className);});复制代码

仅有 5 行逻辑代码。

总结

还有更多使用生成器的方法。例如,在进行异步操作或者按需循环时生成器也非常有用。

我希望这篇文章能帮你更好地理解 JavaScript 生成器。


是一个翻译优质互联网技术文章的社区,文章来源为 上的英文分享文章。内容覆盖 、、、、、、、等领域,想要查看更多优质译文请持续关注 、、。

转载地址:http://bzboa.baihongyu.com/

你可能感兴趣的文章
《VMware、Citrix和Microsoft虚拟化技术详解与应用实践》一2.2 ESXi简介
查看>>
C#反射方法学习
查看>>
MD5加密解密
查看>>
.Net 转战 Android 4.4 日常笔记(6)--Android Studio DDMS用法
查看>>
SVN被锁定的几种解决方法
查看>>
js如何判断是否在iframe中及防止网页被别站用 iframe嵌套 (Load denied by X-Frame-Options)...
查看>>
ios ios7 取消控制拉升
查看>>
182在屏幕中实现网格化视图效果
查看>>
本文摘录 - FlumeJava
查看>>
Scala学习(三)----数组相关操作
查看>>
Matlab基于学习------------------函数微分学
查看>>
Dundas 系列
查看>>
Windows的命令行查看,修改,删除,添加环境变量
查看>>
iOS 图文混排
查看>>
64. Minimum Path Sum
查看>>
Windows Live Writer 使用指南
查看>>
分析iOS Crash文件,使用命令符号化iOS Crash文件
查看>>
R学习笔记 第五篇:字符串操作
查看>>
在Mac OS下配置PHP开发环境
查看>>
(转)介绍下Nuget在传统Asp.net项目中的使用
查看>>