ES6生成器(Generators)简介

我们从一个示例开始:

function* quips(name) {
yield "你好 " + name + "!";
yield "希望你能喜欢这篇介绍ES6的译文";
if (name.startsWith("X")) {
yield "你的名字 " + name + " 首字母是X,这很酷!";
}
yield "我们下次再见!";
}

这段代码看起来很像一个函数,我们称之为生成器函数,它与普通函数有很多共同点,但是二者有如下区别:

  • 普通函数使用function声明,而生成器函数使用function*声明。
  • 在生成器函数内部,有一种类似return的语法:关键字yield。二者的区别是,普通函数只可以return一次,而生成器函数可以yield多次(当然也可以只yield一次)。在生成器的执行过程中,遇到yield表达式立即暂停,后续可恢复执行状态。

这就是普通函数和生成器函数之间最大的区别,普通函数不能自暂停,生成器函数可以。

生成器做了什么?

当你调用quips()生成器函数时发生了什么?

> var iter = quips("jorendorff");
[object Generator]
> iter.next()
{ value: "你好 jorendorff!", done: false }
> iter.next()
{ value: "希望你能喜欢这篇介绍ES6的译文", done: false }
> iter.next()
{ value: "我们下次再见!", done: false }
> iter.next()
{ value: undefined, done: true }

生成器调用看起来非常类似:quips("jorendorff")。但是,当你调用一个生成器时,它并非立即执行,而是返回一个已暂停的生成器对象(上述实例代码中的iter)。你可将这个生成器对象视为一次函数调用,只不过立即冻结了,它恰好在生成器函数的最顶端的第一行代码之前冻结了。

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

这也是在上述代码中我们每次都调用iter.next()的原因,我们获得了quips()函数体中yield表达式生成的不同的字符串值。

调用最后一个iter.next()时,我们最终抵达生成器函数的末尾,所以返回结果中done的值为true。抵达函数的末尾意味着没有返回值,所以返回结果中value的值为undefined。

如果用专业术语描述,每当生成器执行yields语句,生成器的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置)被移出堆栈。然而,生成器对象保留了对这个堆栈结构的引用(备份),所以稍后调用.next()可以重新激活堆栈结构并且继续执行。

值得特别一提的是,生成器不是线程,在支持线程的语言中,多段代码可以同时运行,通通常导致竞态条件和非确定性,不过同时也带来不错的性能。生成器则完全不同。当生成器运行时,它和调用者处于同一线程中,拥有确定的连续执行顺序,永不并发。与系统线程不同的是,生成器只有在其函数体内标记为yield的点才会暂停。

现在,我们了解了生成器的原理,领略过生成器的运行、暂停恢复运行的不同状态。那么,这些奇怪的功能究竟有何用处?

生成器是迭代器!

实现一个接口不是一桩小事,我们一起实现一个迭代器。举个例子,我们创建一个简单的range迭代器,它可以简单地将两个数字之间的所有数相加。首先是传统C的for(;;)循环:

// 应该弹出三次 "ding"
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};
}
}
}

// 返回一个新的迭代器,可以从start到stop计数。
function range(start, stop) {
return new RangeIterator(start, stop);
}

你大概不会为了使迭代器更易于构建从而建议我们为JS语言引入一个离奇古怪又野蛮的新型控制流结构,但是既然我们有生成器,是否可以在这里应用它们呢?一起尝试一下:

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

以上4行代码实现的生成器完全可以替代之前引入了一整个RangeIterator类的23行代码的实现。可行的原因是:生成器是迭代器。所有的生成器都有内建.next()和[Symbol.iterator]()方法的实现。你只须编写循环部分的行为。

我们都非常讨厌被迫用被动语态写一封很长的邮件,不借助生成器实现迭代器的过程与之类似,令人痛苦不堪。当你的语言不再简练,说出的话就会变得难以理解。RangeIterator的实现代码很长并且非常奇怪,因为你需要在不借助循环语法的前提下为它添加循环功能的描述。所以生成器是最好的解决方案!

我们如何发挥作为迭代器的生成器所产生的最大效力?

l 使任意对象可迭代。编写生成器函数遍历这个对象,运行时yield每一个值。然后将这个生成器函数作为这个对象的[Symbol.iterator]方法。

l 简化数组构建函数。假设你有一个函数,每次调用的时候返回一个数组结果,就像这样:

// 拆分一维数组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 (var data of myNewGenerator(args))遍历我们所需的数据。
  • 构建与迭代相关的工具。ES6不提供用来过滤、映射以及针对任意可迭代数据集进行特殊操作的扩展库。借助生成器,我们只须写几行代码就可以实现类似的工具。

举个例子,假设你需要一个等效于Array.prototype.filter并且支持DOM NodeLists的方法,可以这样写:

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

你看,生成器魔力四射!借助它们的力量可以非常轻松地实现自定义迭代器,记住,迭代器贯穿ES6的始终,它是数据和循环的新标准。

生成器和异步代码

异步API拥有错误处理规则,不支持异常处理。不同的API有不同的规则,大多数的错误规则是默认的;在有些API里,甚至连成功提示都是默认的。

这些是到目前为止我们为异步编程所付出的代价,我们正慢慢开始接受异步代码不如等效同步代码美观又简洁的这个事实。

生成器为你提供了避免以上问题的新思路。

实验性的Q.async()尝试结合promises使用生成器产生异步代码的等效同步代码。举个例子:

// 制造一些噪音的同步代码。
function makeNoise() {
shake();
rattle();
roll();
}

// 制造一些噪音的异步代码。
// 返回一个Promise对象
// 当我们制造完噪音的时候会变为resolved
function makeNoise_async() {
return Q.async(function* () {
yield shake_async();
yield rattle_async();
yield roll_async();
});
}

二者主要的区别是,异步版本必须在每次调用异步函数的地方添加yield关键字。

在Q.async版本中添加一个类似if语句的判断或try/catch块,如同向同步版本中添加类似功能一样简单。与其它异步代码编写方法相比,这种方法更自然,不像是学一门新语言一样辛苦。

如果你已经看到这里,你可以试着阅读来自James Long的更深入地讲解生成器的文章

生成器为我们提供了一个新的异步编程模型思路,这种方法更适合人类的大脑。相关工作正在不断展开。此外,更好的语法或许会有帮助,ES7中有一个有关异步函数的提案,它基于promises和生成器构建,并从C#相似的特性中汲取了大量灵感。

如何应用这些疯狂的新特性?

在服务器端,现在你可以在io.js中使用ES6(在Node中你需要使用--harmony这个命令行选项)。

在浏览器端,到目前为止只有Firefox 27+和Chrome 39+支持了ES6生成器。如果要在web端使用生成器,你需要使用BabelTraceur来将你的ES6代码转译为Web友好的ES5。

起初,JS中的生成器由Brendan Eich实现,他的设计参考了Python生成器,而此Python生成器则受到Icon的启发。他们早在2006年就在Firefox 2.0中移植了相关代码。但是,标准化的道路崎岖不平,相关语法和行为都在原先的基础上有所改动。Firefox和Chrome中的ES6生成器都是由编译器hacker Andy Wingo实现的。这项工作由彭博赞助支持(没听错,就是大名鼎鼎的那个彭博!)。

yield;

生成器还有更多未提及的特性,例如:.throw()和.return()方法、可选参数.next()、yield*表达式语法。由于行文过长,估计观众老爷们已然疲乏,我们应该学习一下生成器,暂时yield在这里,剩下的干货择机为大家献上。

ES6最具魔力的特性——生成器的更多相关文章

  1. ES6中的迭代器(Iterator)和生成器(Generator)

    前面的话 用循环语句迭代数据时,必须要初始化一个变量来记录每一次迭代在数据集合中的位置,而在许多编程语言中,已经开始通过程序化的方式用迭代器对象返回迭代过程中集合的每一个元素 迭代器的使用可以极大地简 ...

  2. ES6的十大新特性(转)

    add by zhj: 该文章是由国外一哥们写的,由腾讯前端团队翻译,图片中的妹子长得挺好看的,很养眼,嘿嘿.我目前在学习ES6,这篇文章把ES6的 几个主要新特性进行了归纳总结,犹如脑图一般,让人看 ...

  3. ES6中的迭代器(Iterator)和生成器(Generator)(一)

    用循环语句迭代数据时,必须要初始化一个变量来记录每一次迭代在数据集合中的位置,而在许多编程语言中,已经开始通过程序化的方式用迭代器对象返回迭代过程中集合的每一个元素 迭代器的使用可以极大地简化数据操作 ...

  4. C#面向对象的编程语言具三个特性

    C#面向对象的编程语言具三个特性:有封装性.继承性.多态性 .

  5. 石川es6课程---13-16、generator-认识生成器函数

    石川es6课程---13-16.generator-认识生成器函数 一.总结 一句话总结: ` generator函数,中间可以停,到哪停呢,用 yield 配合,交出执行权 ` 需要调用next() ...

  6. 从 ES6 到 ES10 的新特性万字大总结

    以下文章来源于鱼头的Web海洋 ,作者陈大鱼头   鱼头的Web海洋 一个名为Web的海洋世界 (给前端大全加星标,提升前端技能) 作者:鱼头的Web海洋 公号 / 陈大鱼头 (本文来自作者投稿) 介 ...

  7. 从ES6到ES10的新特性万字大总结

    介绍ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会)在标准ECMA-262中定义的脚本语言规范.这种语言在万维网上应用广泛,它往往被称为JavaScript或JScript,但实 ...

  8. ES6笔记(5)-- Generator生成器函数

    系列文章 -- ES6笔记系列 接触过Ajax请求的会遇到过异步调用的问题,为了保证调用顺序的正确性,一般我们会在回调函数中调用,也有用到一些新的解决方案如Promise相关的技术. 在异步编程中,还 ...

  9. ES6入门系列三(特性总览下)

    0.导言 最近从coffee切换到js,代码量一下子变大了不少,也多了些许陌生感.为了在JS代码中,更合理的使用ES6的新特性,特在此对ES6的特性做一个简单的总览. 1.模块(Module) --C ...

随机推荐

  1. Linux文件/目录权限设置命令:chmod

    文件/目录权限设置命令:chmod 这是Linux系统管理员最常用到的命令之一,它用于改变文件或目录的访问权限.该命令有两种用法: 用包含字母和操作符表达式的文字设定法 其语法格式为:chmod [w ...

  2. zepto的tap事件的点透问题的几种解决方案

    你可能碰到过在页面上创建一个弹出层,弹出层有个关闭的按钮,你点了这个按钮关闭弹出层后,这个按钮正下方的内容也会执行点击事件(或打开链接).这个被定义为这是一个“点透”现象. 以前,我也听到过tap的点 ...

  3. ubuntu安装Lua

    1.网站下载LUA包 curl -R -O http://www.lua.org/ftp/lua-5.2.3.tar.gz 2.下载ubuntu的编译支持sudo apt-get install bu ...

  4. 已知树的前序、中序,求后序的java实现&已知树的后序、中序,求前序的java实现

    public class Order { int findPosInInOrder(String str,String in,int position){ char c = str.charAt(po ...

  5. java二叉树的实现和遍历

    /* * Java实现二叉树 */ public class BinaryTree { int treeNode; BinaryTree leftTree; BinaryTree rightTree; ...

  6. c#中enum的用法小结

    转自:http://blog.csdn.net/moxiaomomo/article/details/8056356 enums枚举是值类型,数据直接存储在栈中,而不是使用引用和真实数据的隔离方式来存 ...

  7. js在输出时乱码

    如果网页头是<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> ...

  8. Network | UDP checksum

    1. 校验和 ICMP,IP,UDP,TCP报头部分都有checksum(检验和)字段.IP 首部里的校验和只校验首部:ICMP.IGMP.TCP和UDP首部中的校验和校验首部和数据. UDP和TCP ...

  9. app与后台通信协议

    通用的语言有很多种,例如英语和中文,在网络的通讯中,通用的协议有很多,其中http是被最广泛使用的.如果是私有的协议,那就只能自己设计了. 用http是最方便的,如果是私有协议,包含协议的封装和拆解, ...

  10. saltstack学习

    1. 创建基础镜像 2. 创建配置文件 3. 启动容器 4. 检查创建是否成功 1. 创建基础镜像 salt-master, 文件名Dockerfile # VERSION 1.0 # TO_BUIL ...