ES6——生成器

什么是生成器?

咱们先从下面的这里例子开始。html

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!";
}

这段代码是一个对话猫,这多是当前网络上最重要的一类应用。程序员

这个在必定程度上看起来像一个函数,对不?这就被称为生成器函数,同时它与函数之间也有不少类似之处。可是你一会儿就能发现两个不一样之处:编程

普通的函数使用function做为开始。生成器函数以function*开始。 在一个生成器函数中,yield是一个关键字,语法和return很类似。 区别在于,一个函数(甚至是生成器函数),只能返回一次,可是一个生成器函数可以yield不少次。 yield表达式暂停生成器的运行,而后它可以在以后从新被使用。 就是这样的,以上就是普通的函数和生成器函数之间的大区别。普通的函数不能本身暂停。然而生成器函数能够本身暂停运行。数组

生成器的用处

当你调用quips()生成器函数时将会发生什么?浏览器

> 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 }

你可能很是习惯于普通的函数以及他们的表现。当你调用他们的时候,他们当即开始运行,当遇到 return或者throw的时候,他们中止运行。任何一个JS程序员都很是习惯于上述的过程。服务器

调用一个生成器看起来是同样的:quips(”jorendorff”)。 可是当你调用一个生成器,他还不开始运行。反而,它返回一个暂停的生成器对象(在上述的例子中被称为iter)。你能够认为这个生成器对象是一个函数调用,暂时中止。特别的是,其在生成器函数一开始就中止了,在运行代码的第一行以前。网络

每次你调用生成器对象的.next()方法,函数将其本身解冻并运行直到其到达下一个yield表达式。并发

这就是上面代码中咱们为何要调用iter.next(),调用后咱们得到一个不一样的字符串值。这些值都是由quips()里的yield表达式产生的。异步

在最后一个iter.next( )调用中,咱们最后结束了生成器函数,因此结果的.done领域的值为true。 一个函数的结束就像是返回undefined,并且这也是为何结果的.value领域是不肯定的。async

如今多是一个好机会来返回到上面的对话猫的例子那页面上,同时真正地能够玩转代码。尝试着在一个循环中加入一个yield。这会发生什么呢?

在技术层面上,每一次一个生成器进行yield操做,其堆栈桢,包括局部变量,参数,临时值,以及在生成器中的执行的当前位置,被从栈中删除。然而,生成器对象保有这个堆栈帧的引用(或者是副本)。所以,接下来的调用.next( )能够从新激活它并继续执行。

值得指出的是,生成器都没有线程。在能使用线程的语言中,多份代码能够在同一时间运行,这一般致使了竞争条件,非肯定性和很是很是好的性能。生成器和这彻底不一样。当生成器运行时,它与调用者运行在同一个线程中。执行的顺序是连续且肯定的,并永远不会并发。不一样于系统线程,生成器只会在代码中用yield标记的地方才会悬挂。

好了。咱们知道生成器是什么了。咱们已经看到了生成器器运行,暂停,而后恢复执行。如今的大问题是,这样奇怪的能力怎么多是有用的呢?

生成器是迭代器,咱们已经看到了ES6的迭代器不仅是一个简单的内置类。他们是语言的扩展点。你能够经过实现两种方法来建立你本身的迭代器,这两种方法是:Symbol.iterator和.next()。

可是,实现接口老是至少仍是有一点工做量的。让咱们来看看一个迭代器实如今实践中看起来是什么样的。由于是一个例子,让咱们使用一个简单的范围(range)迭代器,只简单的从一个数字数到另外一个数字,就像一个老式的C语言的for(;;)循环。

// 这个应该三次发出“叮”的声音

for (var value of range(0, 3)) {
  alert("Ding! at floor #" + value);
}

这里是一个使用ES6的类的解决方案。

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};
    }
  }
}
// 返回一个新的从“开始”数到“结束”的迭代器。

function range(start, stop) {
  return new RangeIterator(start, stop);
}

在实际运行中看这段代码(http://codepen.io/anon/pen/NqGgOQ)。

这就是像是在Java或Swift语言里实现一个迭代器。它不是那么糟糕。但它也并非那么简单。在这个代码中有没有任何错误?这就很差说了。它看起来彻底不像咱们想在这里模仿的原来的for(;;)循环:迭代器协议迫使咱们抛弃了循环。

在这一点上,你可能会对迭代器不太热情。他们可能对使用来讲很棒,但他们彷佛很难实现。

你可能不会建议咱们只是为了简单的建立迭代器,而在JS语言中引进一个复杂的新的控制流结构。可是,由于咱们确实有生成器,咱们能在这里使用它们吗?让咱们试一试:

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

在这里看代码的具体运行(http://codepen.io/anon/pen/mJewga)。

上述的四行的生成器是对先前range()的二十三行的实现的一个直接替代,包括了整个RangeIterator类。这多是由于生成器是迭代器。全部的生成器都有一个内置的对.next()已经Symbol.iterator方法的实现。

不使用生成器来实现迭代器就像是被强迫用被动语气写一封很长的邮件。原本想简单地想表达你的意思,可能到最后你说的会变得至关使人费解。RangeIterator是很长且怪异的,由于它必须不使用循环语法来描述一个循环的功能。生成器是答案。

咱们还能如何使用生成器做为迭代器的能力?

使对象可迭代。只要写一个迭代器函数来一直调用this,在其出现的地方生成每个值。而后以该对象的[Symbol.iterator]方法来安装该生成器函数。

简化数组构建函数。实现一个函数,每当其被调用就会返回一个数组,以下面的这个例子:

// 将一维数组'icons'分为长度为'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;
}

生成器能够将代码缩短不少:

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

在行为上惟一的不一样之处在于,取代一次性计算全部的结果,并返回他们的一个数组,这里返回一个迭代器且其能够按需一个一个地计算结果。

特别大量的结果。你不能构建一个无穷大的数组。可是你能够返回一个生成器,其能够生成一个无限大的序列,同时每个调用者均可以使用它无论他们须要多少个值。

重构复杂的循环。你有庞大而丑陋的函数吗?你是否是想将它分为两个简单的部分呢?生成器就是能够帮助你达成这一目标的成套的重构工具。当你面对一个复杂的循环,你能够将产生数据的代码抽取出来编程一个独立的生成器函数。而后改变循环为for循环(myNewGenerator(args)的var数据)。

使用迭代的工具。ES6不提供扩展的库来进行过滤,映射以及通常能够访问任意可迭代的数据集。但生成器是伟大的,你只须要用几行代码就能够构建你须要的工具。举个例子说,假设你须要一个东西等同于Array.prototype.filter,其是在DOM NodeLists上工做的,不仅是一个数组。这用代码来实现就是小意思:

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

这个就是为何生成器如此有用吗?固然。他们是要实现自定义的迭代器的简单的方法。同时,迭代器在整个ES6中是用于数据和循环的新标准。

可是,这还不是生成器能作的事情的所有。这甚至有可能不是他们作的最重要的事情。

生成器和异步代码

这里是一个 JS 代码,我写了一个 while 的 back 部分。

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

可能这看起来和你代码的一部分比较相像。异步API一般状况下须要一个回调,这就意味着你作一些事情时要写一个额外的匿名函数。因此若是你有一部分代码作三件事情,而不是三行代码,你是在看三个缩进层次的代码。

这里有一些我已经写好的 JS 代码:

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

异步API有错误处理的约定,但不是使用异常。不一样的API有不一样的约定。在他们中的大多数中,错误在默认状况下被默默地删除。其中有一些,即便是普通的圆满完成,在默认状况下都会被删除。

直到如今,这些问题都只是简单的转画为咱们进行异步编程的代价了。咱们已经开始接受异步代码了,他们只是看起来不是像同步代码那样美好和简单。

生成器提供了新的但愿,能够不用这样作的。

Q.async()是一个试验性的尝试。其使用迭代器来生成相似于同步代码的异步代码。举个例子:

// 
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版本中添加一点小东西,如if语句或try/catch块,与在普通同步版本中添加是彻底相同的。相比于编写异步代码的其余方式,有种不是在学习一个全新的语言的感受。

因此生成器指出了一个更适合人类大脑的新的异步编程模型。这项工做正在进行中。除其余事项外,更好的语法可能有所帮助。

异步函数的提出,创建在双方的承诺和生成器的基础上,并从在C#相似的功能中采起灵感,这些都是ES7要作的事情。

我何时能够用这疯狂的东西?

在服务器端,咱们如今能够在io.js中使用 ES6 生成器。若是你启用--harmony选项,咱们在Node中可也已使用ES6生成器。

在浏览器中,现今只有火狐27版本以上以及谷歌浏览器39版本以上的支持ES6生成器。为了在现今的网络上面使用生成器,你将须要使用Babel或者Traceur来将你的ES6的代码翻译为网络友好的ES5代码。

一些重要的事件值得了解

生成器是由布伦丹·艾希首次在JS上实现的。布伦丹·艾希的设计是牢牢跟随由Icon启发的Python生成器。他们早在2006年就运用在火狐2.0版本上了。可是标准化的道路是崎岖不平的,并且语法和行为在这个过程当中改变了不少。ES6生成器是由编程黑客温格安迪在火狐浏览器和谷歌浏览器中实现的。这项工做是由Bloomberg赞助的。

yield

关于生成器还有更多的说法。咱们没有包含.throw()和.return()方法,可选的参数.next(),或yield*表达式语法。但我认为这个帖子已经很长了,且如今已经足够扑朔迷离了。像生成器自己,咱们应该停下来休息一下。

转载:http://www.html-js.com

相关文章
相关标签/搜索