生成值

在前一节中,我们提到了一个generator的有趣用法,作为一种生产值的方式。这 不是 我们本章主要关注的,但如果我们不在这里讲一下基本我们会想念它的,特别是因为这种用法实质上是它的名称的由来:生成器。

我们将要稍稍深入一下 迭代器 的话题,但我们会绕回到它们如何与generator关联,并使用generator来 生成 值。

发生器与迭代器

想象你正在生产一系列的值,它们中的每一个都与前一个值有可定义的关系。为此,你将需要一个有状态的发生器来记住上一个给出的值。

你可以用函数闭包(参加本系列的 作用域与闭包)来直接地实现这样的东西:

  1. var gimmeSomething = (function(){
  2. var nextVal;
  3. return function(){
  4. if (nextVal === undefined) {
  5. nextVal = 1;
  6. }
  7. else {
  8. nextVal = (3 * nextVal) + 6;
  9. }
  10. return nextVal;
  11. };
  12. })();
  13. gimmeSomething(); // 1
  14. gimmeSomething(); // 9
  15. gimmeSomething(); // 33
  16. gimmeSomething(); // 105

注意: 这里nextVal的计算逻辑已经被简化了,但从概念上讲,直到 下一次 gimmeSomething()调用发生之前,我们不想计算 下一个值(也就是nextVal),因为一般对于持久性更强的,或者比简单的number更有限的资源的发生器来说,那可能是一种资源泄漏的设计。

生成随意的数字序列不是是一个很真实的例子。但是如果你从一个数据源中生成记录呢?你可以想象很多相同的代码。

事实上,这种任务是一种非常常见的设计模式,通常用迭代器解决。一个 迭代器 是一个明确定义的接口,用来逐个通过一系列从发生器得到的值。迭代器的JS接口,和大多数语言一样,是在你每次想从发生器中得到下一个值时调用的next()

我们可以为我们的数字序列发生器实现标准的 迭代器

  1. var something = (function(){
  2. var nextVal;
  3. return {
  4. // `for..of`循环需要这个
  5. [Symbol.iterator]: function(){ return this; },
  6. // 标准的迭代器接口方法
  7. next: function(){
  8. if (nextVal === undefined) {
  9. nextVal = 1;
  10. }
  11. else {
  12. nextVal = (3 * nextVal) + 6;
  13. }
  14. return { done:false, value:nextVal };
  15. }
  16. };
  17. })();
  18. something.next().value; // 1
  19. something.next().value; // 9
  20. something.next().value; // 33
  21. something.next().value; // 105

注意: 我们将在“Iterables”一节中讲解为什么我们在这个代码段中需要[Symbol.iterator]: ..这一部分。在语法上讲,两个ES6特性在发挥作用。首先,[ .. ]语法称为一个 计算型属性名(参见本系列的 this与对象原型)。它是一种字面对象定义方法,用来指定一个表达式并使用这个表达式的结果作为属性名。另一个,Symbol.iterator是ES6预定义的特殊Symbol值。

next()调用返回一个对象,它带有两个属性:done是一个boolean值表示 迭代器 的完成状态;value持有迭代的值。

ES6还增加了for..of循环,它意味着一个标准的 迭代器 可以使用原生的循环语法来自动地被消费:

  1. for (var v of something) {
  2. console.log( v );
  3. // 不要让循环永无休止!
  4. if (v > 500) {
  5. break;
  6. }
  7. }
  8. // 1 9 33 105 321 969

注意: 因为我们的something迭代器总是返回done:false,这个for..of循环将会永远运行,这就是为什么我们条件性地放进一个break。对于迭代器来说永不终结是完全没有问题的,但是也有一些情况 迭代器 将运行在有限的值的集合上,而最终返回done:true

for..of循环为每一次迭代自动调用next()——他不会给next()传入任何值——而且他将会在收到一个done:true时自动终结。这对于在一个集合的数据中进行循环十分方便。

当然,你可以手动循环一个迭代器,调用next()并检查done:true条件来知道什么时候停止:

  1. for (
  2. var ret;
  3. (ret = something.next()) && !ret.done;
  4. ) {
  5. console.log( ret.value );
  6. // 不要让循环永无休止!
  7. if (ret.value > 500) {
  8. break;
  9. }
  10. }
  11. // 1 9 33 105 321 969

注意: 这种手动的for方式当然要比ES6的for..of循环语法难看,但它的好处是它提供给你一个机会,在有必要时传值给next(..)调用。

除了制造你自己的 迭代器 之外,许多JS中(就ES6来说)内建的数据结构,比如array,也有默认的 迭代器

  1. var a = [1,3,5,7,9];
  2. for (var v of a) {
  3. console.log( v );
  4. }
  5. // 1 3 5 7 9

for..of循环向a要来它的迭代器,并自动使用它迭代a的值。

注意: 看起来像是一个ES6的奇怪省略,普通的object有意地不带有像array那样的默认 迭代器。原因比我们要在这里讲的深刻得多。如果你想要的只是迭代一个对象的属性(不特别保证顺序),Object.keys(..)返回一个array,它可以像for (var k of Object.keys(obj)) { ..这样使用。像这样用for..of循环一个对象上的键,与用for..in循环内很相似,除了在for..in中会包含[[Prototype]]链的属性,而Object.keys(..)不会(参见本系列的 this与对象原型)。

Iterables

在我们运行的例子中的something对象被称为一个 迭代器,因为它的接口中有next()方法。但一个紧密关联的术语是 iterable,它指 包含有 一个可以迭代它所有值的迭代器的对象。

在ES6中,从一个 iterable 中取得一个 迭代器 的方法是,iterable 上必须有一个函数,它的名称是特殊的ES6符号值Symbol.iterator。当这个函数被调用时,它就会返回一个 迭代器。虽然不是必须的,但一般来说每次调用应当返回一个全新的 迭代器

前一个代码段的a就是一个 iterablefor..of循环自动地调用它的Symbol.iterator函数来构建一个 迭代器。我们当然可以手动地调用这个函数,然后使用它返回的 iterator

  1. var a = [1,3,5,7,9];
  2. var it = a[Symbol.iterator]();
  3. it.next().value; // 1
  4. it.next().value; // 3
  5. it.next().value; // 5
  6. ..

在前面定义something的代码段中,你可能已经注意到了这一行:

  1. [Symbol.iterator]: function(){ return this; }

这段有点让人困惑的代码制造了something值——something迭代器 的接口——也是一个 iterable;现在它既是一个 iterable 也是一个 迭代器。然后,我们把something传递给for..of循环:

  1. for (var v of something) {
  2. ..
  3. }

for..of循环期待something是一个 iterable,所以它会寻找并调用它的Symbol.iterator函数。我们将这个函数定义为简单地return this,所以它将自己给出,而for..of不会知道这些。

Generator迭代器

带着 迭代器 的背景知识,让我们把注意力移回generator。一个generator可以被看做一个值的发生器,我们通过一个 迭代器 接口的next()调用每次从中抽取一个值。

所以,一个generator本身在技术上讲并不是一个 iterable,虽然很相似——当你执行generator时,你就得到一个 迭代器

  1. function *foo(){ .. }
  2. var it = foo();

我们可以用generator实现早前的something无限数字序列发生器,就像这样:

  1. function *something() {
  2. var nextVal;
  3. while (true) {
  4. if (nextVal === undefined) {
  5. nextVal = 1;
  6. }
  7. else {
  8. nextVal = (3 * nextVal) + 6;
  9. }
  10. yield nextVal;
  11. }
  12. }

注意: 在一个真实的JS程序中含有一个while..true循环通常是一件非常不好的事情,至少如果它没有一个breakreturn语句,那么它就很可能永远运行,并同步地,阻塞/锁定浏览器UI。然而,在generator中,如果这样的循环含有一个yield,那它就是完全没有问题的,因为generator将在每次迭代后暂停,yield回主程序和/或事件轮询队列。说的明白点儿,“generator把while..true带回到JS编程中了!”

这变得相当干净和简单点儿了,对吧?因为generator会暂停在每个yield*something()函数的状态(作用域)被保持着,这意味着没有必要用闭包的模板代码来跨调用保留变量的状态了。

不仅是更简单的代码——我们不必自己制造 迭代器 接口了——它实际上是更合理的代码,因为它更清晰地表达了意图。比如,while..true循环告诉我们这个generator将要永远运行——只要我们一直向它请求,它就一直 产生 值。

现在我们可以在for..of循环中使用新得发亮的*something()generator了,而且你会看到它工作起来基本一模一样:

  1. for (var v of something()) {
  2. console.log( v );
  3. // 不要让循环永无休止!
  4. if (v > 500) {
  5. break;
  6. }
  7. }
  8. // 1 9 33 105 321 969

不要跳过for (var v of something()) ..!我们不仅仅像之前的例子那样将something作为一个值引用了,而是调用*something()generator来得到它的 迭代器,并交给for..of使用。

如果你仔细观察,在这个generator和循环的互动中,你可能会有两个疑问:

  • 为什么我们不能说for (var v of something) ..?因为这个something是一个generator,而不是一个 iterable。我们不得不调用something()来构建一个发生器给for..of,以便它可以迭代。
  • something()调用创建一个 迭代器,但是for..of想要一个 iterable,对吧?对,generator的 迭代器 上也有一个Symbol.iterator函数,这个函数基本上就是return this,就像我们刚才定义的somethingiterable。换句话说generator的 迭代器 也是一个 iterable

停止Generator

在前一个例子中,看起来在循环的break被调用后,*something()generator的 迭代器 实例基本上被留在了一个永远挂起的状态。

但是这里有一个隐藏的行为为你处理这件事。for..of循环的“异常完成”(“提前终结”等等)——一般是由breakreturn,或未捕捉的异常导致的——会向generator的 迭代器 发送一个信号,以使它终结。

注意: 技术上讲,for..of循环也会在循环正常完成时向 迭代器 发送这个信号。对于generator来说,这实质上是一个无实际意义的操作,因为generator的 迭代器 要首先完成,for..of循环才能完成。然而,自定义的 迭代器 可能会希望从for..of循环的消费者那里得到另外的信号。

虽然一个for..of循环将会自动发送这种信号,你可能会希望手动发送信号给一个 迭代器;你可以通过调用return(..)来这么做。

如果你在generator内部指定一个try..finally从句,它将总是被执行,即便是generator从外部被完成。这在你需要进行资源清理时很有用(数据库连接等):

  1. function *something() {
  2. try {
  3. var nextVal;
  4. while (true) {
  5. if (nextVal === undefined) {
  6. nextVal = 1;
  7. }
  8. else {
  9. nextVal = (3 * nextVal) + 6;
  10. }
  11. yield nextVal;
  12. }
  13. }
  14. // 清理用的从句
  15. finally {
  16. console.log( "cleaning up!" );
  17. }
  18. }

前面那个在for..of中带有break的例子将会触发finally从句。但是你可以用return(..)从外部来手动终结generator的 迭代器 实例:

  1. var it = something();
  2. for (var v of it) {
  3. console.log( v );
  4. // 不要让循环永无休止!
  5. if (v > 500) {
  6. console.log(
  7. // 使generator得迭代器完成
  8. it.return( "Hello World" ).value
  9. );
  10. // 这里不需要`break`
  11. }
  12. }
  13. // 1 9 33 105 321 969
  14. // cleaning up!
  15. // Hello World

当我们调用it.return(..)时,它会立即终结generator,从而运行finally从句。而且,它会将返回的value设置为你传入return(..)的任何东西,这就是Hellow World如何立即返回来的。我们现在也不必再包含一个break,因为generator的 迭代器 会被设置为done:true,所以for..of循环会在下一次迭代时终结。

generator的命名大部分源自于这种 消费生产的值 的用法。但要重申的是,这只是generator的用法之一,而且坦白的说,在这本书的背景下这甚至不是我们主要关注的。

但是现在我们更加全面地了解它们的机制是如何工作的,我们接下来可以将注意力转向generator如何实施于异步并发。