深入理解ES6 Generators

原文链接:https://hacks.mozilla.org/2015/05/es6-in-depth-generators/

“深入理解ES6”是一个针对第6版ECMAScript(简称ES6)标准的新特性的系列教程。

非常高兴能发布今天的内容。今天我们叫讨论ES6中最“奇妙”的特性。

为什么说“奇妙”呢?首先,这个特性与已存在于JS中的其他特性相比有较大的不同,尤其在刚开始接触时有些晦涩难懂。换句话说,它彻底颠覆了JS的正常行为。如果这还不算“奇妙”,那我不知道什么才算是了。

不仅如此:这个特性简化了代码,并且修正了“回调地狱”(callback hell)的非自然界限。

我说的是否有点过于神秘?让我们深入了解,由你自己判断吧。

介绍ES6 Generators

什么是generators?

首先来看一个例子:

function* quips(name) {
  yield "hello " + name + "!";
  yield "i hope you are enjoying the blog posts";
  if (name.startsWith("X")) {
    yield "it's cool how your name starts with X, " + name;
  }
  yield "see you later!";
}

这是一段“a talking cat”(会说话的猫)的代码,有可能是当今网络最为流行的一款应用。(译者注:原文中提供了一个代码运行的链接,但是点开后无法运行,故在此不做翻译)

代码看起来像是一个function,对吗?这个称作为generator-function(产生器函数),虽然与普通函数有很多的相似之处,但是可以发现两个不同的地方:

  • 普通函数以function开始,而generator-functionfunction*开始
  • generator-function中,存在关键字yield,在语法上与return相似。区别在于,每个函数(即使是generator-function)也只能返回(return)一次,但是一个generator-function可以产生(yield)任意次。yield表达式可以暂停generator-function的执行,并记忆执行位置,以便后续继续执行。

所以普通函数不能暂停自己的运行,但是generator-function可以,这就是二者之间最大的区别。

Generators可以做什么

考虑当调用quips()generator-function时发生了什么?

> var iter = quips("jorendorff");
  [object Generator]
> iter.next()
  { value: "hello jorendorff!", done: false }
> iter.next()
  { value: "i hope you are enjoying the blog posts", done: false }
> iter.next()
  { value: "see you later!", done: false }
> iter.next()
  { value: undefined, done: true }

你可能已经习惯了普通函数以及它们的行为。当你调用它们的时候,他们即刻开始执行,直到正常返回或是抛出错误。这个认知已经是JS程序员的第二本能了。

调用generator-function的方式看起来并没有什么特别:quips("jorendorff")。但是当你调用generator-function的时候,它并没有立刻开始运行,而是返回一个暂停的Generator Object(产生器对象)(在上面对的例子中是iter)。你可以把这个Generator Object想象成一个即刻冻结的函数调用。特别地,它是在generator-function的最顶部就已停止运行,就是在第一行代码之前。

每次当你调用Generator Object.next()方法时,这个方法调用就会自动解冻执行,直到遇到下一个yield表达式。

这也是为什么我们每次调用iter.next()时,会得到不同的结果。这些结果的值便是在quips()函数体内部由yield表达式生成的。

在最后一次调用iter.next()时,我们达到了generator-function的结束位置,所以.done属性的值是true。达到函数的结束为止就好像是返回undefined,这也是为什么.value属性的值是undefined(译者注:即使在函数结束为止添加return表达式,在函数运行结束后也会如此返回,即返回{ value: undefined, done: true }

是时候回到’the talking cat demo page’调试代码的时候了。尝试在循环中添加一个yield,会发什么呢?

从技术上讲,每一次产生一个yield,它的堆栈结构——局部变量,参数,临时变量,和当前在generators内部的执行位置——会从堆栈中移除。无论如何,Generator Object会保留(或者复制)一个对此堆栈结构的引用,一遍在之后调用.next()方法时,可以激活它并继续执行。

值得注意点是generators并非是线程。在具有线程概念的编程语言中,多个代码片可以同时运行,通常会导致竞态条件(race condition),非确定性(nondeterminism)和较优的运行效率。generators与其完全不同。当一个generator运行时,它与调用者运行在同一个线程。执行顺序是线性的、确定的,而且永远不会发生并行(concurrent)现象。也不像系统的线程,一个generator只在遇到函数体内部的yield表达式时才会被暂停。

好了,我们已经知道什么是generators了。也已经了解generator的运行,暂停,和再执行。那么最关键的问题。这个怪异的功能有什么用途呢?

Generators是迭代器(iterators)

上个星期,我看到ES6的iterators不仅仅是内置的类(class),而是JS的一个扩展点。可以通过实现[Symbol.iterator]().next()两个方法来实现自定制的迭代器。

但是实现一个接口(interface)总是有一点工作。让我们先看下在实际中一个iterator的实现是怎么样的。作为例子,我们实现一个简单的iterator叫做range,只是简单的实现一个数字到另一个数字的计数逻辑,就像C语言中的for(;;)循环。

// This should "ding" three times
for (var value of range(0, 3)) {
  alert("Ding! at floor #" + value);
}

以下是使用ES6 class的解决方案。(如果你不是很了解class语法,也不要担心,我们将在后续的博客中讲解)

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

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

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    } else {
      return {done: true, value: undefined};
    }
  }
}

// Return a new iterator that counts up from 'start' to 'stop'.
function range(start, stop) {
  return new RangeIterator(start, stop);
}

查看代码效果

这就是对iterator的基本实现,有点像Java和Swift的实现。感觉还不错。但这些并不重要。在这段代码里面是否存在bugs?这很难说。我们在这里模拟的就是原始的for(;;)循环,没有什么特别的:iterator拟定的规则强迫我们将循环拆开。

此时你可能对iterator的热情有所丧失。他们是很好用但是却难于实现。

可能你无法理解,为什么只是为了更简单的构建iterators,就要为JS添加一个原始的,复杂难懂的新的流程控制结构。但是因为我们有generators,我们是否可以将它用在这里能?让我们尝试一下:

function* range(start, stop) {
  for (var i = start; i < stop; i++)
    yield i;
}

查看代码效果

使用上面只有4行代码的generator实现range()函数,要比之前使用23行代码实现简洁的多,它已经包含了整个RangeIterator类的作用。这是合理的,因为Generators就是迭代器。所有的Generators都内置的实现了.next()[Symbol.iterator]()函数。你需要做的只是编写循环内部的处理逻辑。

不通过Generators实现iterators就好像被强迫只用被动语态去写一封很长的Email。当不采用简单直接的方式来解决问题时,最终的替代方法将会是非常麻烦的。(译者注:在这里,作者的意思应该是如果不使用Generators而使用其他方式实现iterators是一件非常复杂的事情)RangeIterator类的实现就是一种冗长而诡异的实现方式,它目的是实现循环,去无法使用循环的语法。Generators就是解决问题的答案。

Generators这用像iterators的行为还有什么值得我们去利用的吗?

  • 使任意对象可迭代。只需要编写一个generator-function遍历this,并且通过yield产生该对象的所有属性。最后将generator-function装配到对象的[Symbol.iterator]属性上即可。
  • 简化数组构建函数。假设存在一个函数,在每次被调用时,都有一个数组被作为返回值返回。像下面这个:

    // Divide the one-dimensional array 'icons'
    // into arrays of length 'rowLength'.
    function splitIntoRows(icons, rowLength) {
      var rows = [];
      for (var i = 0; i < icons.length; i += rowLength) {
        rows.push(icons.slice(i, i + rowLength));
      }
      return rows;
    }

    Generators可以将代码量减少

    function* splitIntoRows(icons, rowLength) {
      for (var i = 0; i < icons.length; i += rowLength) {
        yield icons.slice(i, i + rowLength);
      }
    }

    唯一的区别是,之前的逻辑是一次计算所有的结果,并且返回数组。而改变后的则是返回一个iterator,并根据命令逐一计算结果。

  • 非正常尺寸的结果。你无法构建一个无限的数组。但是可以返回一个Generator,来返回无穷的序列,而每一个调用者可以获取他所需要的个数的值。

  • 重构复杂的循环。你是否写过冗长、不整洁的函数?你是否想要将它分成两个简单的部分?Generators是一个重构的利器。当你面对复杂的循环时,可以将产生数据的部分提取出来,将其安排到新的generator-function中。然后将循环改写成for (var data of myNewGenerator(args))
  • **处理可迭代对象的工具。**ES6并没有为过滤(filtering),图(mapping)提供额外的库,一般来说都是使用Set数据结构进行hack。但是Generators帮助你可以只用少量的代码来构建你所需要的数据结构。

    例如,假设你需要将Array.prototype.filter方法运用到DOM NodeList,而非只是在数组上使用。实现是如此轻松:

    function* filter(test, iterable) {
      for (var item of iterable) {
        if (test(item))
          yield item;
      }
    }

所以Generators是否有用?当然。它是如此简单的实现自定义iterators,而iterators则是贯穿整个ES6的数据遍历的新规则。

但这并不是Generators的极限。有可能这都不是最主要的功能。

Generators和异步处理

这是之前写的JS代码

          };
        })
      });
    });
  });
});

也许你在自己的代码中见过类似的情况。Asynchronous APIs(异步接口)需要一个回调函数,这就需要每次在你需要进行事务处理时添加额外的匿名函数。所以如果你想要异步处理三个事务逻辑时,往往的到的不是三行代码而是三层级缩进的代码。

以下是更多我写过的JS代码:

}).on('close', function () {
  done(undefined, undefined);
}).on('error', function (error) {
  done(error);
});

Asynchronous APIs有自定义的错误处理惯例而非使用异常处理。不同的APIs有不同的惯例。其中有一些会静默的丢弃错误,更有甚者会静默的丢弃成功完成的处理

到目前为止,这些问题已经成为我们处理异步编程时的代价。我们不得不接受异步代码无法像顺序执行代码那样美观和简洁。

Generators提供了新的解决方式。

Q.async()是对Generators在异步处理的尝试性使用。。只在使异步代码能够像非异步代码一样进行清晰和美观。例如:

// Synchronous code to make some noise.
function makeNoise() {
  shake();
  rattle();
  roll();
}

// Asynchronous code to make some noise.
// Returns a Promise object that becomes resolved
// when we're done making noise.
function makeNoise_async() {
  return Q.async(function* () {
    yield shake_async();
    yield rattle_async();
    yield roll_async();
  });
}

最主要的区别是,异步版本在每次作为异步调用的位置,添加了yield关键字。

Q.async版本中添加iftry/catch代码块就好像是在非异步函数中添加一样。相比其他编写异步代码的方式,这个相较于学习一门新语言代价要小得多。

如果你想更深入的理解这个话题,请参见 James Long的《A Study on Solving Callbacks with JavaScript Generators》

所以说Generators指明了一条更符合人类思维模式的方式,去实现衣服编程。这项工作还在继续。另外更好的语法或许会提供有效的帮助。而异步函数的提案,对promises和Generators的构建,以及从C#提取相应的灵感,已经被排进了ES7的日程。

什么情况可以使用这些令人兴奋的特性

在服务端,你可以通过io.js使用ES6的Generators(如果在Node中你使用了--harmony命令行选项)。

在浏览器中,目前只有Firefox 27+和Chrome 39+支持ES6的Generators。如果想要现在使用Generators,需要使用Babel或Traceur将代码从ES6格式解析成Web可接受的ES5。

需要感谢的是:Generators首先由Brendan Eich在JS中实现;他的设计是根据 Python generators,灵感来源于Icon。在2006年在Firefox 2.0中推出。在将其加入标准化的过程十分艰辛,其语法和行为也在这过程中有所变化。最终ES6的Generators是由 Andy Wingo在Firefox和Chrome中实现。工作是由Bloomberg赞助支持。

yield

Generators还有很多功能。我们并没有讲解.throw().return()方法,.next()方法的可选参数,以及yield*表达式的语法。但是到目前为止,这次讲解已经长的难以理解了 。就像Generators一样,我们需要休息下,在之后再进一步去了解剩下的部分。

下一周,我们将会有做一个小小的改变。这次我们一下讲解了两个深层的概念。聊一聊ES6特性如何改变你的生活岂不是更好?一些简单明显的应用?一些能让你更加轻松的特性?ES6也包含了一些这方面的优化。

即将到来:一个能够正确插入你编写的代码的特性。欢迎参加下周的讲解,深入理解ES6的template strings(模板字符串)

参考资料:
Callback Hell:http://callbackhell.com/

译者注
翻译水平有限,敬请谅解,如有问题,敬请提出,一定及时改正。另,完全直译原文,因为原文发布时间较早,可能对浏览器的支持存在部分差异,请以当前最新浏览器版本为准。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值