专业编程基础技术教程

网站首页 > 基础教程 正文

「翻译」JavaScript的可视化学习之六:生成器和遍历器

ccvgpt 2024-10-23 09:25:04 基础教程 8 ℃

根本停不下来?那你可能需要锻炼了,深呼吸,一提,一松。 - 随时上下车的Generator Functions

请注意上面这张图,生成器函数的那些事,将在这个图上缓缓展开。

「翻译」JavaScript的可视化学习之六:生成器和遍历器

译者术语解释

Generator:生成器

Generator Object:生成器对象

Iterator:遍历器,或者迭代器,文中统一为遍历器


ES6引入了一个很酷的东西,叫做生成器函数(generator functions)。每当我问别人关于生成器函数的时候,他们的回答基本上都是:“我看过一次,弄糊涂了,然后再也没看过”,“哦,天哪,我读了那么多关于生成器函数的文章,但我还是不明白”,“我懂了,但为什么会有人用它呢?”。也许这只是我和自己的对话,因为我过去很长一段时间就是这么想的!但他们其实是很酷的功能。

那么,什么是生成器函数呢?我们先来看一个经典风的函数:

function normalFunction() {
  console.log("I am the coolest function!")
  console.log("There is no way of stopping me!")
  console.log("Oh boi we're still going go")
  console.log("Okay finally all done now")
}

是的,这不是什么特殊的代码!它只是一个普通函数,打印了4次日志。让我们来调用它!

“但是莉迪亚(Lydia),你为什么要我在这个普通又无聊的函数浪费5秒钟的时间”,很好的问题。正常的函数遵循一个叫做根本停不下来(也叫运行到完成run-to-completion)的模型:当我们调用一个函数时,它会一直运行到它完成(好吧,除非某个地方有错误)。我们不能够在函数中随意暂停,多么想要也不行。

现在最酷的部分来了:生成器函数不遵循根本停不下来模型!这是否意味着我们可以在执行生成器函数的过程中随意暂停它?嗯,差不多吧!让我们看看什么是生成器函数,以及如何使用它们。


我们通过在`function`关键字后写一个星号`*`来创建一个生成器函数

function* generatorFunction() { ... }

但这不是我们使用生成器函数所要做的全部!与常规函数相比,生成器函数的工作方式完全不同:

  • 调用生成器函数返回一个生成器对象,它是一个遍历器
  • 我们可以在生成器函数中使用“yield”关键字来“暂停”执行。

但那到底是个什么鬼!?

让我们先看第一个:调用生成器函数返回一个生成器对象。当我们调用一个常规函数时,函数体被执行并最终返回一个值。但是,当我们调用生成器函数时,会返回一个生成器对象!让我们打印一下返回值,看看是什么东东。

我听到了你内心(或外部)的尖叫声,因为它看起来确实有点多。但别担心,您在这里看到的任何属性,我们都不会用到。那么生成器对象有什么用呢?

首先,我们需要往后退一小步,回答常规函数和生成器函数之间的第二个区别:我们可以在生成器函数中使用yield关键字“暂停”执行

使用生成器函数,我们可以编写如下代码(“genFunc”是“generatorFunction”的缩写):

function* genFunc() {
  yield 'star'
  console.log('First log!')
  yield 'love'
  console.log('Second log!')
  return 'Done!'
}

yield”关键字在那里做什么?当生成器遇到“yield”关键字时,它将“暂停”执行。神奇的是,下次我们再次运行函数时,它会记住之前暂停的位置,然后从那里开始运行!基本上,会发生如下情况(别担心,稍后会有动画):

  1. 第一次运行时,它在第一行“暂停”,并返回字符串'star'。
  2. 第二次运行时,它从前一个“yield”关键字的行开始,然后它一直运行到第二个“yield”关键字,并返回字符串'love'。
  3. 第三次运行时,它从前一个“yield”关键字的行开始,一直向下运行,直到遇到“return”关键字,并返回Done!”.

但是。。。我们该如何调用生成器函数返回的生成器对象呢?这就是生成器对象发挥作用的地方!

生成器对象包含一个“next”方法(在原型链上),我们将使用这个方法来遍历生成器对象。但是,为了记住它在上一次yield后离开的状态,我们需要将生成器对象分配给一个变量。这里我用了变量genObj(genObjgeneratorObject的缩写)。

是的,和我们上面看到的一样可怕。让我们看看当我们对生成器对象genObj”调用“next”方法时会发生什么!

生成器一直运行,直到遇到第一个“yield”关键字,该关键字恰好位于第一行!它返回一个对象,包含“value”属性和“done”属性。

{ value: ... , done: ... }

value”属性等于我们yield返回的值。

done”属性是一个布尔值,只有在生成器函数return”而不是yield时,才会将其设置为“true”!

此时对生成器的遍历就停止了,这使得函数看起来像是暂停了!非常神奇。让我们再次调用“next”方法!

首先,我们输出了字符串'First log!'到控制台。这既不是“yield”也不是“return”关键字,所以它继续往下走!然后,它遇到了值为“love”的“yield”关键字。它会返回一个对象,对象包含“value”属性,值为“love”,以及“done”属性,值为“false”,因为我们还没有从生成器函数return”。

我们快要完成了!让我们最后一次调用“next”。

我们输出了字符串'Second log!'到控制台。然后,它遇到了“return”关键字和值“Done!”。它返回一个对象,对象的“value”属性为“Done!”。这次是真的返回了,所以'done'属性的值被设置为'true'!

done”属性实际上非常重要。生成器对象只能遍历一次。什么?!那么当我们再次调用“next”方法时会发生什么呢?

它永远只是简单的返回“undefined”。如果你想再次遍历它,你得创建一个新的生成器对象!


正如我们刚才看到的,生成器函数返回了一个遍历器生成器对象)。但是。。。等一下,遍历器?这是否意味着我们可以在返回的对象上使用'for of'循环和'spread'操作符?没错!

让我们尝试使用`[...]`语法把返回的值展开到一个数组中。

或者用“for of”循环?!

无限可能!


但是是什么让遍历器成为遍历器呢?我们也在数组、字符串、映射和集合上使用'for of'循环和'spread'语法。这实际上是因为它们实现了遍历器协议:`[Symbol.iterator]`。假设我们有以下值(非常具有描述性的名称):

const array = ['star', 'love', 'bird']
const string = 'I love JavaScript!'
const object = {name: "Lydia Hallie"}
function regularFunction() {
  return 'I am a basic function'
}
function* generatorFunction() {
  return 'I am a generator function'
}
const generatorObject = generatorFunction()

array”、“string”和“generatorObject”都是遍历器!让我们看看他们各自`[Symbol.iterator]`属性的值。

那么那些不是遍历器的[Symbol.iterator]属性的值又是什么呢?

是的,就是没有。所以...我们可以简单地给他们加上`[Symbol.iterator]`属性,来将不可遍历变成可遍历吗?是的,我们可以!

`[Symbol.iterator]`必须返回一个遍历器,该遍历器包含一个“next”方法,该方法返回的对象正如我们之前看到:`{value:'…',done:false/true}`。

为了保持简单(就像懒惰的我喜欢做的那样),我们可以简单地设置`[Symbol.iterator]`等于生成器函数,因为它默认返回遍历器。让我们将对象变为可遍历的,并且在函数里返回整个对象:

object[Symbol.iterator] = function* () {
  yield this
}

现在看看当我们在“object”对象上使用'spread'语法或'for-of'循环时会发生什么!

或者我们只想得到对象的键列表。“哦,那很简单,我们只要返回`Object.keys(this)`而不是'this'!”

object[Symbol.iterator] = function* () {
  yield Object.keys(this)
}

嗯,让我们试试。

该死的,`Object.keys(this)`是一个数组,因此返回的值是一个数组。然后我们将这个返回的数组展开到另一个数组中,得到一个嵌套数组。这不是我们想要的,我们只想一个一个的返回键!

有个好消息!在生成器中,我们可以使用'yield*`关键字在遍历器中返回单个值。假设我们有一个生成器函数,它首先返回一个鳄梨,然后我们要一个一个的返回另一个遍历器(在本例中是一个数组)的值。我们可以用'yield*`关键字来实现,然后我们委托给另一个生成器!

在继续遍历“genObj”之前,被委托的生成器的每个值都被单独的yield了。

这才是我们想要的,一个一个的获取对象的所有键!


生成器函数的另一个用途是,我们可以(在某种程度上)将它们用作观察者函数。生成器可以等待传入的数据,并且只有当该数据被传递时,它才会对其进行处理。例如:

function* generatorFunction() {
  const second = yield 'First!'
  console.log(second)
  return 'All done!'
}

和上面例子有一个很大的区别,我们并不只是“yield [value]”。相反,我们定义了一个变量“second”,并返回字符串“First!”!,这是我们第一次调用“next”方法时将得到的值。

让我们看看当调用“next”方法,进行第一次遍历时会发生什么。

它在第一行遇到了“yield”,并返回了值“First!”!`. 那么,变量“second”的值是多少?

这实际上是我们下次调用“next”方法时传递给它的值!这次,让我们传递字符串“I like JavaScript”。

这里需要注意的是,第一次调用“next”方法时还没有跟踪任何输入,我们只是通过第一次调用它来启动观察者。生成器等待我们的输入,然后继续,并可能处理传递给“next”方法的值。


那么,为什么要使用生成器函数呢?

生成器最大的优点之一是,它们是“懒计算”的。这意味着在调用“next”方法后返回的值,只有在我们明确需要的时候才进行计算!普通函数没有这样的功能:所有的值都是为您生成好的,以防将来某个时候你需要使用它。

还有其他几个场景,但我通常喜欢在大数据量时这样做,这会给我更多的控制权!


想象一下我们有一个读书俱乐部的名单!为了使这个例子简短,而不是一个庞大的代码块,每个图书俱乐部只有一个成员。一个成员当前正在阅读几本书,这在books数组中表示!

现在我们要找一本书,id为“ey812”。为了找到这本书,我们可以使用一个嵌套的for循环或“forEach”,但这意味着在我们找到了这个团队成员之后,我们仍然会遍历完所有数据!

生成器很酷的一点就是,它不会持续运行,除非我们让它继续。这意味着我们可以计算每个返回的项,如果它是我们要查找的项,我们就不调用“next”!让我们看看那会是什么样子。

首先,让我们创建一个生成器,它遍历每个团队成员的“books”数组。我们将团队成员的“book”数组传递给函数,遍历该数组,并返回每个book!

function* iterateBooks(books) {
  for(let i = 0; i < books.length; i++) {
    yield books[i]
  }
}

很完美!现在我们要创建一个遍历“clubMembers”数组的生成器。我们并不关心俱乐部会员本身,我们只需要反复遍历他们的书籍。在“iterateMembers”生成器中,让我们委托“iterateBooks”遍历器来返回他们的书!

function* iteratemembers(members) {
  for (let i = 0; i < members.length; i++) {
    const clubMember = members[i]
    yield* iterateBooks(clubMember.books)
  }
}

就要完成了!最后一步是遍历书友俱乐部。就像前面的例子一样,我们并不真正关心读书俱乐部本身,我们只关心俱乐部成员(尤其是他们的书)。让我们委托“iterateClubMembers”遍历器并将“clubMembers”数组传递给它。

function* iterateBookClubs(bookClubs) {
  for (let i = 0; i< bookClubs.length; i++) {
    const bookClub = bookClubs[i]
    yield* iterateMembers(bookClub.clubMembers)
  }
}

为了开始上面的所有遍历,我们将“bookClub”数组传递给“iterateBookClubs”生成器并返回生成器对象,这里我们定义为'it'。

const it = iterateBookClubs(bookClubs)

让我们调用“next”方法,直到得到一本id为“ey812”的书。

不错呦!我们不必为了得到我们要找的书而遍历所有的数据。相反,我们只是根据需要查找数据!


当然,每次手动调用“next”方法不是很有效率的做法...所以让我们来写一个函数吧!

我们把'id'传给函数,这就是我们要查找的书的id。如果`value.id`是我们要查找的id,那就简单地返回整个“value”(book对象),否则,继续调用“next”!

function findBook(id) {
  const genObj = iterateBookClubs(bookClubs)
  let result = genObj.next()
  while (!result.done) {
    if (result.value.id === id) {
      return result.value
    } else {
      result = genObj.next()
    }
  }
}

当然,这是一个很小的数据集。但是想象一下,我们有大量的数据,或者可能有一个传入的数据流,我们需要解析这些数据流,以便只找到某个值。通常,我们必须等待整个数据集准备就绪,才能开始解析。有了生成器函数,我们可以只需要一小部分数据,检查数据,而且只有在调用“next”方法时才会生成值!


如果你仍然不知道“到底发生了什么”,不要担心,生成器函数是很难理解的,除非你真的去使用它,并有一些真实可靠的场景!我希望现在有些概念更清楚一点,而且一如既往:如果你有任何问题,请随时联系!


翻译来源:https://dev.to/lydiahallie/javascript-visualized-generators-and-iterators-e36

Tags:

最近发表
标签列表