返回介绍

第一部分 类型和语法

第二部分 异步和性能

4.2 生成器产生值

发布于 2023-05-24 16:38:21 字数 8213 浏览 0 评论 0 收藏 0

在前面一节中,我们提到生成器的一种有趣用法是作为一种产生值的方式。这并不是本章的重点,但是如果不介绍一些基础的话,就会缺乏完整性了,特别是因为这正是“生成器”这个名称最初的使用场景。

下面要偏一下题,先介绍一点迭代器 ,不过我们还会回来介绍它们与生成器的关系以及如何使用生成器来生成 值。

4.2.1 生产者与迭代器

假定你要产生一系列值,其中每个值都与前面一个有特定的关系。要实现这一点,需要一个有状态的生产者能够记住其生成的最后一个值。

可以实现一个直接使用函数闭包的版本(参见本系列的《你不知道的 JavaScript(上卷)》的“作用域和闭包”部分),类似如下:

var gimmeSomething = (function(){
  var nextVal;

  return function(){
    if (nextVal === undefined) {
      nextVal = 1;
    }
    else {
      nextVal = (3 * nextVal) +6;
    }

    return nextVal;
  };
})();

gimmeSomething();     // 1
gimmeSomething();     // 9
gimmeSomething();     // 33
gimmeSomething();     // 105

这里 nextVal 的计算逻辑已经简化了,但是从概念上说,我们希望直到下一次 gimmeSomething() 调用发生时才计算下一个值(即 nextVal )。否则,一般来说,对更持久化或比起简单数字资源更受限的生产者来说,这可能就是资源泄漏的设计。

生成任意数字序列并不是一个很实际的例子。但如果是想要从数据源生成记录呢?可以采用基本相同的代码。

实际上,这个任务是一个非常通用的设计模式,通常通过迭代器来解决。迭代器 是一个定义良好的接口,用于从一个生产者一步步得到一系列值。JavaScript 迭代器的接口,与多数语言类似,就是每次想要从生产者得到下一个值的时候调用 next() 。

可以为我们的数字序列生成器实现标准的迭代器 接口:

var something = (function(){
  var nextVal;

  return {
    // for..of循环需要
    [Symbol.iterator]: function(){ return this; },

    // 标准迭代器接口方法
    next: function(){
      if (nextVal === undefined) {
        nextVal = 1;
      }
      else {
        nextVal = (3 * nextVal) + 6;
      }

      return { done:false, value:nextVal };
    }
  };
})();
something.next().value;   // 1
something.next().value;   // 9
something.next().value;   // 33
something.next().value;   // 105

我们将在 4.2.2 节解释为什么在这段代码中需要 [Symbol.iterator]: .. 这一部分。从语法上说,这涉及了两个 ES6 特性。首先,[ .. ] 语法被称为计算属性名 (参见本系列的《你不知道的 JavaScript(上卷)》的“this 和对象原型”部分)。这在对象术语定义中是指,指定一个表达式并用这个表达式的结果作为属性的名称。另外,Symbol.iterator 是 ES6 预定义的特殊 Symbol 值之一(参见本系列的《你不知道的 JavaScript(下卷)》的“ES6 & Beyond”部分)。

next() 调用返回一个对象。这个对象有两个属性:done 是一个 boolean 值,标识迭代器 的完成状态;value 中放置迭代值。

ES6 还新增了一个 for..of 循环,这意味着可以通过原生循环语法自动迭代标准迭代器

for (var v of something) {
  console.log( v );

  // 不要死循环!
  if (v > 500) {
    break;
  }
}
// 1 9 33 105 321 969

因为我们的迭代器 something 总是返回 done:false ,因此这个 for..of 循环将永远运行下去,这也就是为什么我们要在里面放一个 break 条件。迭代器永不结束是完全没问题的,但是也有一些情况下,迭代器 会在有限的值集合上运行,并最终返回 done:true 。

for..of 循环在每次迭代中自动调用 next() ,它不会向 next() 传入任何值,并且会在接收到 done:true 之后自动停止。这对于在一组数据上循环很方便。

当然,也可以手工在迭代器上循环,调用 next() 并检查 done:true 条件来确定何时停止循环:

for (
  var ret;
  (ret = something.next()) && !ret.done;
) {
  console.log( ret.value );

  // 不要死循环!
  if (ret.value > 500) {
    break;
  }
}
// 1 9 33 105 321 969

这种手工 for 方法当然要比 ES6 的 for..of 循环语法丑陋,但其优点是,这样就可以在需要时向 next() 传递值。

除了构造自己的迭代器 ,许多 JavaScript 的内建数据结构(从 ES6 开始),比如 array ,也有默认的迭代器

var a = [1,3,5,7,9];

for (var v of a) {
  console.log( v );
}
// 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 循环类似,除了 Object.keys(..) 并不包含来自于 [[Prototype]] 链 上的属性,而 for..in 则包含(参见本系列的《你不知道的 JavaScript(上卷)》的“this 和对象原型”部分)。

4.2.2 iterable

前面例子中的 something 对象叫作迭代器 ,因为它的接口中有一个 next() 方法。而与其紧密相关的一个术语是 iterable(可迭代),即指一个包含可以在其值上迭代的迭代器的对象。

从 ES6 开始,从一个 iterable 中提取迭代器的方法是:iterable 必须支持一个函数,其名称是专门的 ES6 符号值 Symbol.iterator 。调用这个函数时,它会返回一个迭代器。通常每次调用会返回一个全新的迭代器,虽然这一点并不是必须的。

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

var a = [1,3,5,7,9];

var it = a[Symbol.iterator]();

it.next().value;  // 1
it.next().value;  // 3
it.next().value;  // 5
..

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

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

这段有点令人疑惑的代码是在将 something 的值(迭代器 something 的接口)也构建成为一个 iterable。现在它既是 iterable,也是迭代器。然后我们把 something 传给 for..of 循环:

for (var v of something) {
  ..
}

for..of 循环期望 something 是 iterable,于是它寻找并调用它的 Symbol.iterator 函数。我们将这个函数定义为就是简单的 return this ,也就是把自身返回,而 for..of 循环并不知情。

4.2.3 生成器迭代器

了解了迭代器的背景,让我们把注意力转回生成器上。可以把生成器看作一个值的生产者,我们通过迭代器接口的 next() 调用一次提取出一个值。

所以,严格说来,生成器本身并不是 iterable,尽管非常类似——当你执行一个生成器,就得到了一个迭代器:

function *foo(){ .. }

var it = foo();

可以通过生成器实现前面的这个 something 无限数字序列生产者,类似这样:

function *something() {
  var nextVal;

  while (true) {
    if (nextVal === undefined) {
      nextVal = 1;
    }
    else {
      nextVal = (3 * nextVal) + 6;
    }

    yield nextVal;
  }
}

通常在实际的 JavaScript 程序中使用 while..true 循环是非常糟糕的主意,至少如果其中没有 break 或 return 的话是这样,因为它有可能会同步地无限循环,并阻塞和锁住浏览器 UI。但是,如果在生成器中有 yield 的话,使用这样的循环就完全没有问题。因为生成器会在每次迭代中暂停,通过 yield 返回到主程序或事件循环队列中。简单地说就是:“生成器把 while..true 带回了 JavaScript 编程的世界!”

这样就简单明确多了,是不是?因为生成器会在每个 yield 处暂停,函数 *something() 的状态(作用域)会被保持,即意味着不需要闭包在调用之间保持变量状态。

这段代码不仅更简洁,我们不需要构造自己的迭代器接口,实际上也更合理,因为它更清晰地表达了意图。比如,while..true 循环告诉我们这个生成器就是要永远运行:只要我们一直索要,它就会一直生成值。

现在,可以通过 for..of 循环使用我们雕琢过的新的 *something() 生成器。你可以看到,其工作方式基本是相同的:

for (var v of something()) {
  console.log( v );

  // 不要死循环!
  if (v > 500) {
    break;
  }
}
// 1 9 33 105 321 969

但是,不要忽略了这段 for (var v of something()) .. !我们并不是像前面的例子那样把 something 当作一个值来引用,而是调用了 *something() 生成器以得到它的迭代器供 for..of 循环使用。

如果认真思考的话,你也许会从这段生成器与循环的交互中提出两个问题。

· 为什么不能用 for (var v of something) .. ?因为这里的 something 是生成器,并不是 iterable。我们需要调用 something() 来构造一个生产者供 for..of 循环迭代。

· something() 调用产生一个迭代器,但 for..of 循环需要的是一个 iterable,对吧?是的。生成器的迭代器也有一个 Symbol.iterator 函数,基本上这个函数做的就是 return this ,和我们前面定义的 iterable something 一样。换句话说,生成器的迭代器也是一个 iterable !

停止生成器

在前面的例子中,看起来似乎 *something() 生成器的迭代器实例在循环中的 break 调用之后就永远留在了挂起状态。

其实有一个隐藏的特性会帮助你管理此事。for..of 循环的“异常结束”(也就是“提前终止”),通常由 break 、return 或者未捕获异常引起,会向生成器的迭代器发送一个信号使其终止。

严格地说,在循环正常结束之后,for..of 循环也会向迭代器发送这个信号。对于生成器来说,这本质上是没有意义的操作,因为生成器的迭代器需要先完成 for..of 循环才能结束。但是,自定义的迭代器可能会需要从 for..of 循环的消费者那里接收这个额外的信号。

尽管 for..of 循环会自动发送这个信号,但你可能会希望向一个迭代器手工发送这个信号。可以通过调用 return(..) 实现这一点。

如果在生成器内有 try..finally 语句,它将总是运行,即使生成器已经外部结束。如果需要清理资源的话(数据库连接等),这一点非常有用:

function *something() {
  try {
    var nextVal;

    while (true) {
      if (nextVal === undefined) {
        nextVal = 1;
      }
      else {
        nextVal = (3 * nextVal) + 6;
      }

      yield nextVal;
    }
  }
  // 清理子句
  finally {
    console.log( "cleaning up!" );
  }
}

之前的例子中,for..of 循环内的 break 会触发 finally 语句。但是,也可以在外部通过 return(..) 手工终止生成器的迭代器实例:

var it = something();
for (var v of it) {
  console.log( v );

  // 不要死循环!
  if (v > 500) {
    console.log(
      // 完成生成器的迭代器
      it.return( "Hello World" ).value
    );
    // 这里不需要break
  }
}
// 1 9 33 105 321 969
// 清理!
// Hello World

调用 it.return(..) 之后,它会立即终止生成器,这当然会运行 finally 语句。另外,它还会把返回的 value 设置为传入 return(..) 的内 容,这也就是 "Hello World" 被传出去的过程。现在我们也不需要包含 break 语句了,因为生成器的迭代器已经被设置为 done:true ,所以 for..of 循环会在下一个迭代终止。

生成器的名字大多来自这种消费生产值 (consuming produced values)的用例。但是,这里要再次申明,这只是生成器的用法之一,坦白地说,甚至不是这本书重点关注的用途。

既然对生成器的工作机制有了更完整的理解,那接下来就可以把关注转向如何把生成器应用于异步并发了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文