深入浅出ES6(三):生成器 Generators
作者 Jason Orendorff github主页 https://github.com/jorendorff
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 }
你大概已经习惯了普通函数的使用方式,当你调用它们时,它们立即开始运行,直到遇到return或抛出异常时才退出执行,作为JS程序员你一定深谙此道。
生成器调用看起来非常类似:quips("jorendorff")。但是,当你调用一个生成器时,它并非立即执行,而是返回一个已暂停的生成器对象(上述实例代码中的iter)。你可将这个生成器对象视为一次函数调用,只不过立即冻结了,它恰好在生成器函数的最顶端的第一行代码之前冻结了。
每当你调用生成器对象的.next()方法时,函数调用将其自身解冻并一直运行到下一个yield表达式,再次暂停。
这也是在上述代码中我们每次都调用iter.next()的原因,我们获得了quips()函数体中yield表达式生成的不同的字符串值。
调用最后一个iter.next()时,我们最终抵达生成器函数的末尾,所以返回结果中done的值为true。抵达函数的末尾意味着没有返回值,所以返回结果中value的值为undefined。
现在回到会说话的猫的demo页面,尝试在循环中加入一个yield,会发生什么?
如果用专业术语描述,每当生成器执行yields语句,生成器的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置)被移出堆栈。然而,生成器对象保留了对这个堆栈结构的引用(备份),所以稍后调用.next()可以重新激活堆栈结构并且继续执行。
值得特别一提的是,生成器不是线程,在支持线程的语言中,多段代码可以同时运行,通通常导致竞态条件和非确定性,不过同时也带来不错的性能。生成器则完全不同。当生成器运行时,它和调用者处于同一线程中,拥有确定的连续执行顺序,永不并发。与系统线程不同的是,生成器只有在其函数体内标记为yield的点才会暂停。
现在,我们了解了生成器的原理,领略过生成器的运行、暂停恢复运行的不同状态。那么,这些奇怪的功能究竟有何用处?
生成器是迭代器!
上周,我们学习了ES6的迭代器,它是ES6中独立的内建类,同时也是语言的一个扩展点,通过实现[Symbol.iterator]()和.next()两个方法你就可以创建自定义迭代器。
实现一个接口不是一桩小事,我们一起实现一个迭代器。举个例子,我们创建一个简单的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);
}
这里的实现类似Java或Swift中的迭代器,不是很糟糕,但也不是完全没有问题。我们很难说清这段代码中是否有bug,这段代码看起来完全不像我们试图模仿的传统for (;;)循环,迭代器协议迫使我们拆解掉循环部分。
此时此刻你对迭代器可能尚无感觉,他们用起来很酷,但看起来有些难以实现。
你大概不会为了使迭代器更易于构建从而建议我们为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的始终,它是数据和循环的新标准。
以上只是生成器的冰山一角,最重要的功能请继续观看!
生成器和异步代码
这是我在一段时间以前写的一些JS代码
};
})
});
});
});
});
可能你已经在自己的代码中见过类似的片段,异步API通常需要一个回调函数,这意味着你需要为每一次任务执行编写额外的异步函数。所以如果你有一段代码需要完成三个任务,你将看到类似的三层级缩进的代码,而非简单的三行代码。
后来我就这样写了:
}).on('close', function () {
done(undefined, undefined);
}).on('error', function (error) {
done(error);
});
异步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端使用生成器,你需要使用Babel或Traceur来将你的ES6代码转译为Web友好的ES5。
起初,JS中的生成器由Brendan Eich实现,他的设计参考了Python生成器,而此Python生成器则受到Icon的启发。他们早在2006年就在Firefox 2.0中移植了相关代码。但是,标准化的道路崎岖不平,相关语法和行为都在原先的基础上有所改动。Firefox和Chrome中的ES6生成器都是由编译器hacker Andy Wingo实现的。这项工作由彭博赞助支持(没听错,就是大名鼎鼎的那个彭博!)。
深入浅出ES6(三):生成器 Generators的更多相关文章
- 深入浅出ES6(十一):生成器 Generators,续篇
作者 Jason Orendorff github主页 https://github.com/jorendorff 欢迎回到深入浅出ES6专栏,望你在ES6探索之旅中收获知识与快乐!程序员们在工作 ...
- ES6学习笔记<三> 生成器函数与yield
为什么要把这个内容拿出来单独做一篇学习笔记? 生成器函数比较重要,相对不是很容易理解,单独做一篇笔记详细聊一聊生成器函数. 标题为什么是生成器函数与yield? 生成器函数类似其他服务器端语音中的接口 ...
- 深入浅出ES6(十七):展望未来
作者 Jason Orendorff github主页 https://github.com/jorendorff 出于对文章长度的考虑,我们还保留了一些尚未提及的新特性,在最后的这篇文章中我会集 ...
- 深入浅出 ES6:ES6 与 Babel / Broccoli 的联用
深入浅出 ES6指的是添加在 ECMASript 标准第六版中的 JavaScript 编程语言的新特性,简称为 ES6. 虽然 ES6 刚刚到来,但是人们已经开始谈论 ES7 了,它未来的样子,以及 ...
- 深入浅出ES6(九):学习Babel和Broccoli,马上就用ES6
作者 Jason Orendorff github主页 https://github.com/jorendorff 现在,我们将向你分步展示如何做到的这一切.上面提及的工具被称为转译器,你可以将它 ...
- 深入浅出ES6(八):Symbols
作者 Jason Orendorff github主页 https://github.com/jorendorff 你是否知道ES6中的Symbols是什么,它有什么作用呢?我相信你很可能不知道, ...
- 深入浅出ES6(六):解构 Destructuring
作者 Jason Orendorff github主页 https://github.com/jorendorff 什么是解构赋值? 解构赋值允许你使用类似数组或对象字面量的语法将数组和对象的属性 ...
- 深入浅出ES6(二):迭代器和for-of循环
作者 Jason Orendorff github主页 https://github.com/jorendorff 我们如何遍历数组中的元素?20年前JavaScript刚萌生时,你可能这样实现数 ...
- 深入浅出ES6(十五):子类 Subclassing
作者 Jason Orendorff github主页 https://github.com/jorendorff 在之前的文章<深入浅出ES6(十三):类 Class>中,我们一起深 ...
- 函数原型属性-JavaScript深入浅出(三)
前两次总结了JavaScript中的基本数据类型(值类型<引用类型>,引用类型<复杂值>)以及他们在内存中的存储,对内存空间有了一个简单的了解,以及第二次总结了this深入浅出 ...
随机推荐
- C#工具介绍
VisualStudio是微软的官方提供的.NET开发工具. 除了VisualStudio外,还有一些开源的.NET开发IDE. MonoDevelop.SharpDevelop等. 开发未必需要使用 ...
- 更新win7资源管理器
更新exeplorer.exe: 1.方法1: void RefreshExplorer() { ]; SHELLEXECUTEINFOA shellExeInfo={}; shellExeInfo. ...
- 《Mail电子邮件日志存储的管理》RedHat6.3——以一举三
我们都知道很多日志的模块都是放在这下面的 要是想修改或是添加其他服务的日志,怎么办?修改下面的配置文件 测试下是否ok 轮转日志和定位分析和分析日志汇总报告: 安装logwatch软件包,这个没啥说的 ...
- 使用tortoisegit管理git 和 权限验证
1 安装Git 2 安装Tortoise 3 在git.oschina 或者 github上创建项目 4 Tortoise配置 TortoiseGit 使用扩展名为ppk的密钥,而不是ssh-keyg ...
- CSS1-CSS3 <color>颜色知识知多少?
非本人原创,原文转载自http://www.zhangxinxu.com/wordpress/2015/07/know-css1-css3-color/ by zhangxinxu from http ...
- HTML:form表单总结,input,select,option,textarea,label
<form>标签是块级元素. form标签的标准属性有id,class,style,title,lang,xml:lang. 表单能够包含input元素(包含button,checkbox ...
- Nginx+Tomcat动静分离
需求:nginx处理用户请求的静态页面,tomcat处理用户请求jsp页面,来实现动态分离,nginx处理静态页面效率远高于tomcat,这样一来就能更好的提高并发,处理性能. 准备软件: 下载jdk ...
- 通过获取客户端Json数据字符串,反序列化为实体对象的一段代码
#region 保存候选人数据 /// <summary> /// 保存候选人数据 /// </summary> /// <param name="entity ...
- 1101. Quick Sort (25)
There is a classical process named partition in the famous quick sort algorithm. In this process we ...
- Oracle计算两个整数的和与这两个整数的差与商
PL/SQL(Procedural Language/SQL)是一种过程化语言. PL/SQL都是以(BLOCK)块为基本单位,整个PL/SQL块分为三部分 1.声明(Declare) 2.执行(以B ...