原文地址
迁移到:http://www.bdata-cap.com/newsinfo/1741515.html
本文内容
- ECMAScript 发生了什么变化?
- 新标准
- 版本号6
- 兑现承诺
- 迭代器和for-of循环
- 生成器 Generators
- 模板字符串
- 不定参数和默认参数
- 解构 Destructuring
- 箭头函数 Arrow Functions
- Symbols
- 集合
- 学习Babel和Broccoli,马上就用ES6
- 代理 Proxies
ES6 说自己的宗旨是“凡是新加入的特性,势必已在其它语言中得到强有力的实用性证明。”——TRUE!如果你大概浏览下 ES6 的新特性,事实上它们都不是什么新东西,而是在其他语言中已经被广泛认可和采用的,还有就是多年工程实践的结果,比如,JavaScript 框架 jQuery、Undercore、AnjularJS、Backbone、React、Ember、Polymer、Knockout 和 Browserify、RequireJS、Webpack,以及NPM 和 Bower,涉及到 JavaScript 的库和框架、模块打包器及测试、任务调度器、包和工作流管理等方面,以前需要用这些三方框架来实现,有些现在则不用了。因为,ES6 本身就具备。所以,以后写 JS 代码,或多或少跟像 Java、C# 等这些服务器端语言有点像~
如果你嫌内容太长,可以大概浏览一下也行~你会发现服务器编程语言很多特性,现在在前端也能使用了~
ECMAScript 发生了什么变化?
JavaScript是ECMAScript的实现和扩展,由ECMA(一个类似W3C的标准组织)参与进行标准化。ECMAScript定义了:
- 语言语法 – 语法解析规则、关键字、语句、声明、运算符等。
- 类型 – 布尔型、数字、字符串、对象等。
- 原型和继承
- 内建对象和函数的标准库 – JSON、Math、数组方法、对象自省方法等。
ECMAScript标准不定义HTML或CSS的相关功能,也不定义类似DOM(文档对象模型)的Web API,这些都在其他的标准中定义。
ECMAScript涵盖了各种环境中JS的使用场景,无论是浏览器环境还是类似node.js的非浏览器环境。
新标准
2015年6月,ECMAScript语言规范第6版最终草案提请Ecma大会审查,这意味着什么呢?——我们将迎来最新的JavaScript核心语言标准。
早在2009年,上一版的ES5,自那时起,ES标准委员会一直在紧锣密鼓地筹备新的JS语言标准——ES6。
ES6是一次重大的版本升级,与此同时,由于ES6秉承着最大化兼容已有代码的设计理念,你过去编写的JS代码将继续正常运行。事实上,许多浏览器已经支持部分ES6特性,并将继续努力实现其余特性。这意味着,在一些已经实现部分特性的浏览器中,你的JS代码已经可以正常运行。如果到目前为止你尚未遇到任何兼容性问题,那么你很有可能将不会遇到这些问题,浏览器正飞速实现各种新特性。
版本号6
ECMAScript标准的历史版本分别是1、2、3、5。
为什么没有版4?其实,的确曾经计划发布具有大量新特性的版4,但最终因想法太过激进而惨遭废除(这一版标准中曾经有一个极其复杂的支持泛型和类型推断的内建静态类型系统)。步子不能迈得太大~
ES4饱受争议,当标准委员会最终停止开发ES4时,其成员同意发布一个相对谦和的ES5版本,随后继续制定一些更具实质性的新特性。这一明确的协商协议最终命名为“Harmony”,因此,ES5规范中包含这样两句话:
ECMAScript是一门充满活力的语言,并在不断进化中。
未来版本的规范中将持续进行重要的技术改进。
兑现承诺
2009年的版5,引入了Object.create()、Object.defineProperty()、getters 和 setters、严格模式以及JSON对象。我已经使用过所有这些新特性,并且非常喜欢。但这些改进并没有影响我编写JS代码的方式,对我来说,最大的革新就是新的数组方法:.map()、. filter()。
但ES6并非如此!经过持续几年的磨砺,它已成为JS有史以来最实质的升级,新的语言和库特性就像无主之宝,等待有识之士的发掘。新特性涵盖范围甚广,小到受欢迎的语法糖,例如箭头函数(arrow functions)和简单的字符串插值(string interpolation),大到烧脑的新概念,例如代理(proxies)和生成器(generators)。
ES6将彻底改变你编写JS代码的方式!
下面从一个经典的“遗漏特性”说起,十年来我一直期待在JavaScript中看到的它——ES6迭代器(iterators)和新的for-of循环!
迭代器和for-of循环
如何遍历数组?20年前JavaScript刚萌生时,你可能这样实现数组遍历:
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
自 ES5 正式发布后,你可以使用内建的 forEach 方法来遍历数组:
myArray.forEach(function (value) {
console.log(value);
});
这段代码看起来更简洁,但有一个小缺陷:不能使用 break 语句中断循环,也不能使用 return 语句返回到外层函数。
当然,如果只用 for 循环的语法来遍历数组元素,那么,你一定想尝试一下 for-in 循环:
for (var index in myArray) {
console.log(myArray[index]);
}
但这绝对是一个糟糕的选择,为什么呢?
- 这段代码中,赋给 index 的值不是实际的数字,而是字符串“0”、“1”、“2”,此时很可能在无意间进行字符串算数计算,例如:“2” + 1 == “21”,这带来极大的不便。
- 作用于数组的for-in循环体除了遍历数组元素外,还会遍历自定义属性。举个例子,如果你的数组中有一个可枚举属性 myArray.name,循环将额外执行一次,遍历到名为“name”的索引。就连数组原型链上的属性都能被访问到。
- 最让人震惊的是,在某些情况下,这段代码可能按照随机顺序遍历数组元素。
- 简而言之,for-in是为普通对象设计的,你可以遍历得到字符串类型的键,因此不适用于数组遍历。
强大的for-of循环
目前来看,成千上万的Web网站依赖 for-in 循环,其中一些网站甚至将其用于数组遍历。如果想通过修正for-in循环增加数组遍历支持会让这一切变得更加混乱,因此,标准委员会在ES6中增加了一种新的循环语法来解决目前的问题。像下面那样:
for (var value of myArray) {
console.log(value);
}
是的,与之前的内建方法相比,这种循环方式看起来是否有些眼熟?那好,我们将要探究一下 for-of 循环的外表下隐藏着哪些强大的功能。现在,只需记住:
- 这是最简洁、最直接的遍历数组元素的语法;
- 与forEach()不同的是,它可以正确响应 break、continue 和 return 语句;
- 这个方法避开了for-in循环的所有缺陷。
for-in循环用来遍历对象属性。for-of循环用来遍历数据—例如数组中的值。
但是,不仅如此!
for-of循环也可以遍历其它的集合。for-of循环不仅支持数组,还支持大多数类数组对象,例如DOM NodeList对象。for-of循环也支持字符串遍历,它将字符串视为一系列的Unicode字符来进行遍历:
for (var chr of "") {
alert(chr);
}
它同样支持遍历 Map 和 Set 对象。
对不起,你一定没听说过Map和Set对象。他们是ES6中新增的类型。我们将在后面讲解这两个新的类型。如果你曾在其它语言中使用过Map和Set,你会发现ES6中并无太大出入。
举个例子,Set 对象可以自动排除重复项:
// 基于单词数组创建一个set对象
var uniqueWords = new Set(words);
生成 Set 对象后,你可以轻松遍历它所包含的内容:
for (var word of uniqueWords) {
console.log(word);
}
Map 对象稍有不同。数据由键值对组成,所以你需要使用解构(destructuring)来将键值对拆解为两个独立的变量:
for (var [key, value] of phoneBookMap) {
console.log(key + "'s phone number is: " + value);
}
解构也是ES6的新特性,我们将在后面讲解。
现在,你只需记住:未来的JS可以使用一些新型的集合类,甚至会有更多的类型陆续诞生,而for-of就是为遍历所有这些集合特别设计的循环语句。
for-of循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用for-in循环(这也是它的本职工作)或内建的Object.keys()方法:
// 向控制台输出对象的可枚举属性
for (var key of Object.keys(someObject)) {
console.log(key + ": " + someObject[key]);
}
深入理解
“能工摹形,巧匠窃意。”——巴勃罗·毕加索
ES6始终坚持这样的宗旨:凡是新加入的特性,势必已在其它语言中得到强有力的实用性证明。
for-of 循环这个新特性,像极了 C++、Java、C# 以及 Python 中的 foreach 循环语句。与它们一样,for-of循环支持语言和标准库中提供的几种不同的数据结构。它同样也是这门语言中的一个扩展点。
正如其它语言中的for/foreach语句一样,for-of循环语句通过方法调用来遍历各种集合。数组、Maps对象、Sets对象以及其它在我们讨论的对象有一个共同点,它们都有一个迭代器方法。
你可以给任意类型的对象添加迭代器方法。
当你为对象添加myObject.toString()方法后,就可以将对象转化为字符串,同样地,当你向任意对象添加myObject[Symbol.iterator]()方法,就可以遍历这个对象了。
举个例子,假设你正在使用jQuery,尽管你非常钟情于里面的.each()方法,但你还是想让jQuery对象也支持for-of循环,可以这样做:
// 因为jQuery对象与数组相似
// 可以为其添加与数组一致的迭代器方法
jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
好的,我知道你在想什么,那个[Symbol.iterator]语法看起来很奇怪,这段代码到底做了什么呢?这里通过Symbol处理了一下方法的名称。标准委员会可以把这个方法命名为.iterator()方法,但是如果你的代码中的对象可能也有一些.iterator()方法,这一定会让你感到非常困惑。于是在ES6标准中使用symbol来作为方法名,而不是使用字符串。
你大概也猜到了,Symbols是ES6中的新类型,我们会在后续的文章中讲解。现在,你需要记住,基于新标准,你可以定义一个全新的 symbol,就像Symbol.iterator,如此一来可以保证不与任何已有代码产生冲突。这样做的代价是,这段代码的语法看起来会略显生硬,但是这微乎其微代价却可以为你带来如此多的新特性和新功能,并且你所做的这一切可以完美地向后兼容。
所有拥有[Symbol.iterator]()的对象被称为可迭代的。在接下来的文章中你会发现,可迭代对象的概念几乎贯穿于整门语言之中,不仅是for-of循环,还有Map和Set构造函数、解构赋值,以及新的展开操作符。
迭代器对象
现在,你将无须亲自从零开始实现一个对象迭代器,我们会在下一篇文章详细讲解。为了帮助你理解本文,我们简单了解一下迭代器(如果你跳过这一章,你将错过非常精彩的技术细节)。
for-of循环首先调用集合的[Symbol.iterator]()方法,紧接着返回一个新的迭代器对象。迭代器对象可以是任意具有.next()方法的对象;for-of循环将重复调用这个方法,每次循环调用一次。举个例子,这段代码是我能想出来的最简单的迭代器:
var zeroesForeverIterator = {
[Symbol.iterator]: function () {
return this;
},
next: function () {
return {done: false, value: 0};
}
};
每一次调用.next()方法,它都返回相同的结果,返回给for-of循环的结果有两种可能:(a) 我们尚未完成迭代;(b) 下一个值为0。这意味着(value of zeroesForeverIterator) {}将会是一个无限循环。当然,一般来说迭代器不会如此简单。
这个迭代器的设计,以及它的.done和.value属性,从表面上看与其它语言中的迭代器不太一样。在Java中,迭代器有分离的.hasNext()和.next()方法。在Python中,他们只有一个.next() 方法,当没有更多值时抛出StopIteration异常。但是所有这三种设计从根本上讲都返回了相同的信息。
迭代器对象也可以实现可选的.return()和.throw(exc)方法。如果for-of循环过早退出会调用.return()方法,异常、 break语句或return语句均可触发过早退出。如果迭代器需要执行一些清洁或释放资源的操作,可以在.return()方法中实现。大多数迭代器方法无须实现这一方法。.throw(exc)方法的使用场景就更特殊了:for-of循环永远不会调用它。但是我们还是会在下一篇文章更详细地讲解它的作用。
现在我们已了解所有细节,可以写一个简单的for-of循环然后按照下面的方法调用重写被迭代的对象。
首先是for-of循环:
for (VAR of ITERABLE) {
// do something
}
然后是一个使用以下方法和少许临时变量实现的与之前大致相当的示例:
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
VAR = $result.value;
// do something
$result = $iterator.next();
}
这段代码没有展示.return()方法是如何处理的,我们可以添加这部分代码,但我认为这对于我们正在讲解的内容来说过于复杂了。for-of循环用起来很简单,但是其背后有着非常复杂的机制。
我何时可以开始使用这一新特性?
目前,对于for-of循环新特性,所有最新版本Firefox都(部分)支持(译注:从FF 13开始陆续支持相关功能,FF 36 - FF 40基本支持大部分特性),在Chrome中可以通过访问 chrome://flags 并启用“实验性JavaScript”来支持。微软的Spartan浏览器支持,但是IE不支持。如果你想在web环境中使用这种新语法,同时需要支持 IE和Safari,你可以使用Babel或Google的Traceur这些编译器来将你的ES6代码翻译为Web友好的ES5代码。
而在服务端,你不需要类似的编译器,io.js中默认支持ES6新语法(部分),在Node中需要添加--harmony选项来启用相关特性。
{done: true}
for-of 循环的使用远没有结束。
在ES6中有一种新的对象与for-of循环配合使用非常契合,后面将讲解。我认为这种新特性是ES6种最梦幻的地方——ES6 的生成器:generators,如果你尚未在类似Python和C#的语言中遇到它,你一开始很可能会发现它令人难以置信,但是这是编写迭代器最简单的方式,在重构中非常有用,并且它很可能改变我们书写异步代码的方式,无论是在浏览器环境还是服务器环境 。
生成器 Generators
为什么说是“最具魔力的”?对于初学者来说,此特性与JS之前已有的特性截然不同,可能会觉得有点晦涩难懂。但是,从某种意义上来说,它使语言内部的常态行为变得更加强大,如果这都不算有魔力,我不知道还有什么能算。
不仅如此,此特性可以极大地简化代码,它甚至可以帮助你逃离“回调地狱”。
既然新特性如此神奇,那么就一起深入了解它的魔力吧!
什么是生成器?
我们从一个示例开始:
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实现的。这项工作由Bloomberg赞助支持(没听错,就是大名鼎鼎的那个彭博!)。
生成器还有更多未提及的特性,例如:.throw()和.return()方法、可选参数.next()、yield*表达式语法。由于行文过长,估计观众已然疲乏,我们应该学习一下生成器,暂时yield在这里,剩下的干货择机为大家献上。
下一次,我们变换一下风格,由于我们接连搬了两座大山:迭代器和生成器,下次就一起研究下不会改变你编程风格的ES6特性好不?就是一些简单又实用的东西,你一定会喜笑颜开哒!你还别说,在什么都要“微”一下的今天,ES6当然要有微改进了!
续篇
回顾
在第三篇文章中,我们着重讲解了生成器的基本行为。你可能对此感到陌生,但是并不难理解。生成器函数与普通函数有很多相似之处,它们之间最大的不同是,普通函数一次执行完毕,而生成器函数体每次执行一部分,每当执行到一个yield表达式的时候就会暂停。
尽管在那篇文章中我们进行过详细解释,但我们始终未把所有特性结合起来给大家讲解示例。现在就让我们出发吧!
function* somewords() {
yield "hello";
yield "world";
}
for (var word of somewords()) {
alert(word);
}
这段脚本简单易懂,但是如果你把代码中不同的比特位当做戏剧中的任务,你会发现它变得如此与众不同。穿上新衣的代码看起来是这样的:
(译者注:下面这是原作者创作的一个剧本,他将ES6中的各种函数和语法拟人化,以讲解生成器(Generator)的实现原理)
场景 - 另一个世界的计算机,白天
for loop女士独自站在舞台上,戴着一顶安全帽,手里拿着一个笔记板,上面记载着所有的事情。
for loop:
(电话响起)
somewords()!
generator出现:这是一位高大的、有着一丝不苟绅士外表的黄铜机器人。
它看起来足够友善,但给人的感觉仍然是冷冰冰的金属。
for loop:
(潇洒地拍了拍她的手)
好吧!我们去找些事儿做吧。
(对generator说)
.next()!
generator动了起来,就像突然拥有了生命。
generator:
{value: "hello", done: false}
然而猝不及防的,它以一个滑稽的姿势停止了动作。
for loop:
alert!
alert小子飞快冲进舞台,眼睛大睁,上气不接下气。我们感觉的到他一向如此。
for loop:
对user说“hello”。
alert小子转身冲下舞台。
alert:
(舞台下,大声尖叫)
一切都静止了!
你正在访问的页面说,
“hello”!
停留了几秒钟后,alert小子跑回舞台,穿过所有人滑停在for loop女士身边。
alert:
user说ok。
for loop:
(潇洒地拍了拍她的手)
好吧!我们去找些事儿做吧。
(回到generator身边)
.next()!
generator又一次焕发生机。
generator:
{value: "world", done: false}
它换了个姿势又一次冻结。
for loop:
alert!
alert:
(已经跑起来)
正在搞定!
(舞台下,大声尖叫)
一切都静止了!
你正在访问的页面说,
“world”!
又一次暂停,然后alert突然跋涉回到舞台,垂头丧气的。
alert:
user再一次说ok,但是…
但是请阻止这个页面
创建额外的对话。
他噘着嘴离开了。
for loop:
(潇洒地拍了拍她的手)
好吧!我们去找些事儿做吧。
(回到generator身边)
.next()!
generator第三次焕发生机。
generator:
(庄严的)
{value: undefined, done: true}
它的头低下了,光芒从它的眼里消失。它不再移动。
for loop
我的午餐时间到了。
她离开了。
一会儿,garbage collector(垃圾收集器)老头进入,捡起了奄奄一息的generator,将它带下舞台。
好吧,这一出戏不太像哈姆雷特,但你应该可以想象得出来。
正如你在戏剧中看到的,当生成器对象第一次出现时,它立即暂停了。每当调用它的.next()
方法,它都会苏醒并向前执行一部分。
所有动作都是单线程同步的。请注意,无论何时永远只有一个真正活动的角色,角色们不会互相打断,亦不会互相讨论,他们轮流讲话,只要他们的话没有说完都可以继续说下去。(就像莎士比亚一样!)
每当for-of
循环遍历生成器时,这出戏的某个版本就展开了。这些.next()
方法调用序列永远不会在你的代码的任何角落出现,在剧本里我把它们都放在舞台上了,但是对于你和你的程序而言,所有这一切都应该在幕后完成,因为生成器和for-of
循环就是被设计成通过迭代器接口联结工作的。
所以,总结一下到目前为止所有的一切:
- 生成器对象是可以产生值的优雅的黄铜机器人。
- 每个生成器函数体构成的单一代码块就是一个机器人。
如何关停生成器
我在第1部分没有提到这些繁琐的生成器特性:
- generator.return()
- generator.next()的可选参数
- generator.throw(error)
- yield*
如果你不理解这些特性存在得意义,就很难对它们提起兴趣,更不用说理解它们的实现细节,所以我选择直接跳过。但是当我们深入学习生成器时,势必要仔细了解这些特性的方方面面。
你或许曾使用过这样的模式:
function dothings() {
setup();
try {
// ... 做一些事情
} finally {
cleanup();
}
}
dothings();
清理(cleanup)过程包括关闭连接或文件,释放系统资源,或者只是更新dom来关闭“运行中”的加载动画。我们希望无论任务成功完成与否都触发清理操作,所以执行流入到finally
代码块。
那么生成器中的清理操作看起来是什么样的呢?
function* producevalues() {
setup();
try {
// ... 生成一些值
} finally {
cleanup();
}
}
for (var value of producevalues()) {
work(value);
}
这段代码看起来很好,但是这里有一个问题:我们没在try
代码块中调用work(value)
,如果它抛出异常,我们的清理步骤会如何执行呢?
或者假设for-of
循环包含一条break
语句或return
语句。清理步骤又会如何执行呢?
放心,清理步骤无论如何都会执行,ES6已经为你做好了一切。
我们第一次讨论迭代器和for-of循环时曾说过,迭代器接口支持一个可选的.return()
方法,每当迭代在迭代器返回{done:true}
之前退出都会自动调用这个方法。生成器支持这个方法,mygenerator.return()
会触发生成器执行任一finally
代码块然后退出,就好像当前的生成暂停点已经被秘密转换为一条return
语句一样。
注意,.return()
方法并不是在所有的上下文中都会被自动调用,只有当使用了迭代协议的情况下才会触发该机制。所以也有可能生成器没执行finally
代码块就直接被垃圾回收了。
如何在舞台上模拟这些特性?生成器被冻结在一个需要一些配置的任务(例如,建造一幢摩天大楼)中间。突然有人抛出一个错误!for
循环捕捉到这个错误并将它放置在一遍,她告诉生成器执行.return()
方法。生成器冷静地拆除了所有脚手架并停工。然后for
循环取回错误,继续执行正常的异常处理过程。
生成器主导模式
到目前为止,我们在剧本中看到的生成器(generator)和使用者(user)之间的对话非常有限,现在换一种方式继续解释:
在这里使用者主导一切流程,生成器根据需要完成它的任务,但这不是使用生成器进行编程的唯一方式。
在第1部分中我曾经说过,生成器可以用来实现异步编程,完成你用异步回调或promise链所做的一切。我知道你一定想知道它是如何实现的,为什么yield的能力(这可是生成器专属的特殊能力)足够应对这些任务。毕竟,异步代码不仅产生(yield)数据,还会触发事件,比如从文件或数据库中调用数据,向服务器发起请求并返回事件循环来等待异步过程结束。生成器如何实现这一切?它又是如何不借助回调力量从文件、数据库或服务器中接受数据?
为了开始找出答案,考虑一下如果.next()
的调用者只有一种方法可以传值返回给生成器会发生什么?仅仅是这一点改变,我们就可能创造一种全新的会话形式:
事实上,生成器的.next()
方法接受一个可选参数,参数稍后会作为yield
表达式的返回值出现在生成器中。那就是说,yield
语句与return
语句不同,它是一个只有当生成器恢复时才会有值的表达式。
var results = yield getdataandlatte(request.areacode);
这一行代码完成了许多功能:
- 调用
getdataandlatte()
,假设函数返回我们在截图中看到的字符串“get me the database records for area code...
”。 - 暂停生成器,生成字符串值。
- 此时可以暂停任意长的时间。
- 最终,直到有人调用
.next({data: ..., coffee: ...})
,我们将这个对象存储在本地变量results
中并继续执行下一行代码。
下面这段代码完整地展示了这一行代码完整的上下文会话:
function* handle(request) {
var results = yield getdataandlatte(request.areacode);
results.coffee.drink();
var target = mosturgentrecord(results.data);
yield updatestatus(target.id, "ready");
}
yield
仍然保持着它的原始含义:暂停生成器,返回值给调用者。但是确实也发生了变化!这里的生成器期待来自调用者的非常具体的支持行为,就好像调用者是它的行政助理一样。
普通函数则与之不同,通常更倾向于满足调用者的需求。但是你可以借助生成器创造一段对话,拓展生成器与其调用者之间可能存在的关系。
这个行政助理生成器运行器可能是什么样的?它大可不必很复杂,就像这样:
function rungeneratoronce(g, result) {
var status = g.next(result);
if (status.done) {
return; // phew!
}
// 生成器请我们去获取一些东西并且
// 当我们搞定的时候再回调它
doasynchronousworkincludingespressomachineoperations(
status.value,
(error, nextresult) => rungeneratoronce(g, nextresult));
}
为了让这段代码运行起来,我们必须创建一个生成器并且运行一次,像这样:
rungeneratoronce(handle(request), undefined);
在之前的文章中,我一个库的示例中提到Q.async()
,在那个库中,生成器是可以根据需要自动运行的异步过程。rungeneratoronce
正式这样的一个具体实现。事实上,生成器一般会生成Promise对象来告诉调用者要做的事情,而不是生成字符串来大声告诉他们。
如果你已经理解了Promise的概念,现在又理解了生成器的概念,你可以尝试修改rungeneratoronce
的代码来支持Promise。这个任务不简单,但是一旦成功,你将能够用Promise线性书写复杂的异步算法,而不仅仅通过.then()
方法或回调函数来实现异步功能。
如何销毁生成器
你是否有看到rungeneratoronce
的错误处理过程?答案一定是没有,因为上面的示例中直接忽略了错误!
是的,那样做不好,但是如果我们想要以某种方法给生成器报告错误,可以尝试一下这个方法:当有错误产生时,不要继续调用generator.next(result)
方法,而应该调用generator.throw(error)
方法来抛出yield
表达式,进而像.return()
方法一样终止生成器的执行。但是如果当前的生成暂停点在一个try
代码块中,那么会catch
到错误并执行finally
代码块,生成器就恢复执行了。
另一项艰巨的任务来啦,你需要修改rungeneratoronce
来确保.throw()
方法能够被恰当地调用。请记住,生成器内部抛出的异常总是会传播到调用者。所以无论生成器是否捕获错误,generator.throw(error)
都会抛出error
并立即返回给你。
当生成器执行到一个yield
表达式并暂停后可以实现以下功能:
- 调用
generator.next(value)
,生成器从离开的地方恢复执行。 - 调用
generator.return()
,传递一个可选值,生成器只执行finally
代码块并不再恢复执行。 - 调用
generator.throw(error)
,生成器表现得像是yield
表达式调用一个函数并抛出错误。 - 或者,什么也不做,生成器永远保持冻结状态。(是的,对于一个生成器来说,很可能执行到一个
try
代码块,永不执行finally
代码块。这种状态下的生成器可以被垃圾收集器回收。)
看起来生成器函数与普通函数的复杂度相当,只有.return()
方法显得不太一样。
事实上,yield
与函数调用有许多共通的地方。当你调用一个函数,你就暂时停止了,对不对?你调用的函数取得主导权,它可能返回值,可能抛出错误,或者永远循环下去。
结合生成器实现更多功能
我再展示一个特性。假设我们写一个简单的生成器函数联结两个可迭代对象:
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
es6支持这样的简写方式:
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
普通yield
表达式只生成一个值,而yield*
表达式可以通过迭代器进行迭代生成所有的值。
这个语法也可以用来解决另一个有趣的问题:在生成器中调用生成器。在普通函数中,我们可以从将一个函数重构为另一个函数并保留所有行为。很显然我们也想重构生成器,但我们需要一种调用提取出来的子例程的方法,我们还需要确保,子例程能够生成之前生成的每一个值。yield*
可以帮助我们实现这一目标。
function* factoredoutchunkofcode() { ... }
function* refactoredfunction() {
...
yield* factoredoutchunkofcode();
...
}
考虑一下这样一个场景:一个黄铜机器人将子任务委托给另一个机器人,函数对组织同步代码来说至关重要,所以这种思想可以使基于生成器特性的大型项目保持简洁有序。
模板字符串
反撇号(`)基础知识
ES6引入了一种新型的字符串字面量语法,我们称之为模板字符串(template strings)。除了使用反撇号字符 ` 代替普通字符串的引号 ' 或 " 外,它们看起来与普通字符串并无二致。在最简单的情况下,它们与普通字符串的表现一致:
context.fillText(`Ceci n'est pas une chaîne.`, x, y);
但我们不能说:“原来只是被反撇号括起来的普通字符串啊”。模板字符串为JavaScript提供了简单的字符串插值功能,从此以后,你可以通过一种更加美观、更加方便的方式向字符串中插值了。这在 Java 和 C# 中早已经有了,不用再用 + 符号连接字符串,用起来很方便~
模板字符串的使用方式成千上万,但最让我暖心的是将其应用于毫不起眼的错误消息提示:
function authorize(user, action) {
if (!user.hasPrivilege(action)) {
throw new Error(
`用户 ${user.name} 未被授权执行 ${action} 操作。`);
}
}
在这个示例中,${user.name} 和 ${action} 被称为模板占位符,JavaScript将把user.name和action的值插入到最终生成的字符串中,例如:用户jorendorff未被授权打冰球。(这是真的,我还没有获得冰球许可证。)
到目前为止,我们所了解到的仅仅是比 + 运算符更优雅的语法,下面是你可能期待的一些特性细节:
- 模板占位符中的代码可以是任意JavaScript表达式,所以函数调用、算数运算等这些都可以作为占位符使用,你甚至可以在一个模板字符串中嵌套另一个,我称之为模板套构(template inception)。
- 如果这两个值都不是字符串,可以按照常规将其转换为字符串。例如:如果action是一个对象,将会调用它的.toString()方法将其转换为字符串值。
- 如果你需要在模板字符串中书写反撇号,你必须使用反斜杠将其转义:`\``等价于"`"。
- 同样地,如果你需要在模板字符串中引入字符$和{。无论你要实现什么样的目标,你都需要用反斜杠转义每一个字符:`\$`和`\{`。
与普通字符串不同的是,模板字符串可以多行书写:
$("#warning").html(`
小心!>/h1>
未经授权打冰球可能受罚
将近${maxPenalty}分钟。
`);
模板字符串中所有的空格、新行、缩进,都会原样输出在生成的字符串中。
好啦,我说过要让你们轻松掌握模板字符串,从现在起难度会加大,你可以到此为止,去喝一杯咖啡,慢慢消化之前的知识。真的,及时回头不是一件令人感到羞愧的事情。Lopes Gonçalves曾经向我们证明过,船只不会被海妖碾压,也不会从地球的边缘坠落下去,他最终跨越了赤道,但是他有继续探索整个南半球么?并没有,他回家了,吃了一顿丰盛的午餐,你一定不排斥这样的感觉。
反撇号的未来
当然,模板字符串也并非事事包揽:
- 它们不会为你自动转义特殊字符,为了避免跨站脚本漏洞,你应当像拼接普通字符串时做的那样对非置信数据进行特殊处理。
- 它们无法很好地与国际化库(可以帮助你面向不同用户提供不同的语言)相配合,模板字符串不会格式化特定语言的数字和日期,更别提同时使用不同语言的情况了。
- 它们不能替代模板引擎的地位,例如:Mustache、Nunjucks。
模板字符串没有内建循环语法,所以你无法通过遍历数组来构建类似HTML中的表格,甚至它连条件语句都不支持。你当然可以使用模板套构(template inception)的方法实现,但在我看来这方法略显愚钝啊。
不过,ES6为JS开发者和库设计者提供了一个很好的衍生工具,你可以借助这一特性突破模板字符串的诸多限制,我们称之为标签模板(tagged templates)。
标签模板的语法非常简单,在模板字符串开始的反撇号前附加一个额外的标签即可。我们的第一个示例将添加一个SaferHTML标签,我们要用这个标签来解决上述的第一个限制:自动转义特殊字符。
请注意,ES6标准库不提供类似SaferHTML功能,我们将在下面自己来实现这个功能。
var message =
SaferHTML`${bonk.sender} 向你示好。
`;
这里用到的标签是一个标识符SaferHTML;也可以使用属性值作为标签,例如:SaferHTML.escape;还可以是一个方法调用,例如:SaferHTML.escape({unicodeControlCharacters: false})。精确地说,任何ES6的成员表达式(MemberExpression)或调用表达式(CallExpression)都可作为标签使用。
可以看出,无标签模板字符串简化了简单字符串拼接,标签模板则完全简化了函数调用!
上面的代码等效于:
var message =
SaferHTML(templateData, bonk.sender);
templateData是一个不可变数组,存储着模板所有的字符串部分,由JS引擎为我们创建。因为占位符将标签模板分割为两个字符串的部分,所以这个数组内含两个元素,形如Object.freeze(["
", " has sent you a bonk.
"]。(事实上,templateData中还有一个属性,在这篇文章中我们不会用到,但是它是标签模板不可分割的一环:templateData.raw,它同样是一个数组,存储着标签模板中所有的字符串部分,如果我们查看源码将会发现,在这里是使用形如\n的转义序列分行,而在templateData中则为真正的新行,标准标签String.raw会用到这些原生字符串。)
如此一来,SaferHTML函数就可以有成千上万种方法来解析字符串和占位符。
在继续阅读以前,可能你苦苦思索到底用SaferHTML来做什么,然后着手尝试去实现它,归根结底,它只是一个函数,你可以在Firefox的开发者控制台里测试你的成果。
以下是一种可行的方案(在gist中查看):
function SaferHTML(templateData) {
var s = templateData[0];
for (var i = 1; i < arguments.length; i++) {
var arg = String(arguments[i]);
// 转义占位符中的特殊字符。
s += arg.replace(/&/g, "&")
.replace(/"<")
.replace(/">");
// 不转义模板中的特殊字符。
s += templateData[i];
}
return s;
}
通过这样的定义,标签模板SaferHTML`
${bonk.sender} 向你示好。
` 可能扩展为字符串 "ES6<3er 向你示好。
"。即使一个恶意命名的用户,例如“黑客Steve