原文地址javascript
ES6 说本身的宗旨是“凡是新加入的特性,势必已在其它语言中获得强有力的实用性证实。”——TRUE!若是你大概浏览下 ES6 的新特性,事实上它们都不是什么新东西,而是在其余语言中已经被普遍承认和采用的,还有就是多年工程实践的结果,好比,JavaScript 框架 jQuery、Undercore、AnjularJS、Backbone、React、Ember、Polymer、Knockout 和 Browserify、RequireJS、Webpack,以及NPM 和 Bower,涉及到 JavaScript 的库和框架、模块打包器及测试、任务调度器、包和工做流管理等方面,之前须要用这些三方框架来实现,有些如今则不用了。由于,ES6 自己就具有。因此,之后写 JS 代码,或多或少跟像 Java、C# 等这些服务器端语言有点像~html
若是你嫌内容太长,能够大概浏览一下也行~你会发现服务器编程语言不少特性,如今在前端也能使用了~前端
JavaScript是ECMAScript的实现和扩展,由ECMA(一个相似W3C的标准组织)参与进行标准化。ECMAScript定义了:html5
ECMAScript标准不定义HTML或CSS的相关功能,也不定义相似DOM(文档对象模型)的Web API,这些都在其余的标准中定义。java
ECMAScript涵盖了各类环境中JS的使用场景,不管是浏览器环境仍是相似node.js的非浏览器环境。node
2015年6月,ECMAScript语言规范第6版最终草案提请Ecma大会审查,这意味着什么呢?——咱们将迎来最新的JavaScript核心语言标准。python
早在2009年,上一版的ES5,自那时起,ES标准委员会一直在紧锣密鼓地筹备新的JS语言标准——ES6。jquery
ES6是一次重大的版本升级,与此同时,因为ES6秉承着最大化兼容已有代码的设计理念,你过去编写的JS代码将继续正常运行。事实上,许多浏览器已经支持部分ES6特性,并将继续努力实现其他特性。这意味着,在一些已经实现部分特性的浏览器中,你的JS代码已经能够正常运行。若是到目前为止你还没有遇到任何兼容性问题,那么你颇有可能将不会遇到这些问题,浏览器正飞速实现各类新特性。git
ECMAScript标准的历史版本分别是一、二、三、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循环!
如何遍历数组?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]);
}
但这绝对是一个糟糕的选择,为何呢?
目前来看,成千上万的Web网站依赖 for-in 循环,其中一些网站甚至将其用于数组遍历。若是想经过修正for-in循环增长数组遍历支持会让这一切变得更加混乱,所以,标准委员会在ES6中增长了一种新的循环语法来解决目前的问题。像下面那样:
for (var value of myArray) {
console.log(value);
}
是的,与以前的内建方法相比,这种循环方式看起来是否有些眼熟?那好,咱们将要探究一下 for-of 循环的外表下隐藏着哪些强大的功能。如今,只需记住:
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#的语言中遇到它,你一开始极可能会发现它使人难以置信,可是这是编写迭代器最简单的方式,在重构中很是有用,而且它极可能改变咱们书写异步代码的方式,不管是在浏览器环境仍是服务器环境 。
为何说是“最具魔力的”?对于初学者来讲,此特性与JS以前已有的特性大相径庭,可能会以为有点晦涩难懂。可是,从某种意义上来讲,它使语言内部的常态行为变得更增强大,若是这都不算有魔力,我不知道还有什么能算。
不只如此,此特性能够极大地简化代码,它甚至能够帮助你逃离“回调地狱”。
既然新特性如此神奇,那么就一块儿深刻了解它的魔力吧!
咱们从一个示例开始:
function* quips(name) {
yield "你好 " + name + "!";
yield "但愿你能喜欢这篇介绍ES6的译文";
if (name.startsWith("X")) {
yield "你的名字 " + name + " 首字母是X,这很酷!";
}
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);
}
}
行为上惟一的不一样是,传统写法当即计算全部结果并返回一个数组类型的结果,使用生成器则返回一个迭代器,每次根据须要逐一地计算结果。
举个例子,假设你须要一个等效于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部分没有提到这些繁琐的生成器特性:
若是你不理解这些特性存在得意义,就很难对它们提起兴趣,更不用说理解它们的实现细节,因此我选择直接跳过。可是当咱们深刻学习生成器时,势必要仔细了解这些特性的方方面面。
你或许曾使用过这样的模式:
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未被受权打冰球。(这是真的,我尚未得到冰球许可证。)
到目前为止,咱们所了解到的仅仅是比 + 运算符更优雅的语法,下面是你可能期待的一些特性细节:
与普通字符串不一样的是,模板字符串能够多行书写:
$("#warning").html(`
<h1>当心!>/h1>
<p>未经受权打冰球可能受罚
将近${maxPenalty}分钟。</p>
`);
模板字符串中全部的空格、新行、缩进,都会原样输出在生成的字符串中。
好啦,我说过要让大家轻松掌握模板字符串,从如今起难度会加大,你能够到此为止,去喝一杯咖啡,慢慢消化以前的知识。真的,及时回头不是一件使人感到羞愧的事情。Lopes Gonçalves曾经向咱们证实过,船只不会被海妖碾压,也不会从地球的边缘坠落下去,他最终跨越了赤道,可是他有继续探索整个南半球么?并无,他回家了,吃了一顿丰盛的午饭,你必定不排斥这样的感受。
固然,模板字符串也并不是事事包揽:
模板字符串没有内建循环语法,因此你没法经过遍历数组来构建相似HTML中的表格,甚至它连条件语句都不支持。你固然能够使用模板套构(template inception)的方法实现,但在我看来这方法略显愚钝啊。
不过,ES6为JS开发者和库设计者提供了一个很好的衍生工具,你能够借助这一特性突破模板字符串的诸多限制,咱们称之为标签模板(tagged templates)。
标签模板的语法很是简单,在模板字符串开始的反撇号前附加一个额外的标签便可。咱们的第一个示例将添加一个SaferHTML标签,咱们要用这个标签来解决上述的第一个限制:自动转义特殊字符。
请注意,ES6标准库不提供相似SaferHTML功能,咱们将在下面本身来实现这个功能。
var message =
SaferHTML`<p>${bonk.sender} 向你示好。</p>`;
这里用到的标签是一个标识符SaferHTML;也能够使用属性值做为标签,例如:SaferHTML.escape;还能够是一个方法调用,例如:SaferHTML.escape({unicodeControlCharacters: false})。精确地说,任何ES6的成员表达式(MemberExpression)或调用表达式(CallExpression)均可做为标签使用。
能够看出,无标签模板字符串简化了简单字符串拼接,标签模板则彻底简化了函数调用!
上面的代码等效于:
var message =
SaferHTML(templateData, bonk.sender);
templateData是一个不可变数组,存储着模板全部的字符串部分,由JS引擎为咱们建立。由于占位符将标签模板分割为两个字符串的部分,因此这个数组内含两个元素,形如Object.freeze(["<p>", " has sent you a bonk.</p>"]。
(事实上,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(/</g, "<")
.replace(/</g, ">");
// 不转义模板中的特殊字符。
s += templateData[i];
}
return s;
}
经过这样的定义,标签模板SaferHTML`<p>${bonk.sender} 向你示好。</p>` 可能扩展为字符串 "<p>ES6<3er 向你示好。</p>"。即便一个恶意命名的用户,例如“黑客Steve<script>alert('xss');< /script>”,向其余用户发送一条骚扰信息,不管如何这条信息都会被转义为普通字符串,其余用户不会受到潜在攻击的威胁。
(顺便一提,若是你感受上述代码中在函数内部使用参数对象的方式令你感到枯燥乏味,不妨期待下一篇,ES6中的另外一个新特性必定会让你眼前一亮!)
仅一个简单的示例不足以说明标签模板的灵活性,咱们一块儿回顾下咱们以前有关模板字符串限制的列表,看一下你还能作些什么不同的事情。
事实上,你能够作的比那更好。
站在安全角度来讲,我实现的SaferHTML函数至关脆弱,你须要经过多种不一样的方式将HTML不一样部分的特殊字符转义,SaferHTML就没法作到所有转义。可是稍加努力,你就能够写出一个更加智能的SaferHTML函数,它能够针对templateData中字符串中的HTML位进行解析,分析出哪个占位符是纯HTML;哪个是元素内部属性,须要转义'和";哪个是URL的query字符串,须要进行URL转义而非HTML转义,等等。智能SaferHTML函数能够将每一个占位符都正确转义。
HTML的解析速度很慢,这种方法听起来是否略显牵强?幸运的是,当模板从新求值的时候标签模板的字符串部分是不改变的。SaferHTML能够缓存全部的解析结果,来加速后续的调用。(缓存能够按照ES6的另外一个特性——WeakMap的形式进行存储,咱们将在将来的文章中继续深刻讨论。)
i18n`Hello ${name}, you have ${amount}:c(CAD) in your bank account.`
// => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.
注意观察这个示例中的运行细节,name和amount都是JavaScript,进行正常插值处理,可是有一段不同凡响的代码,:c(CAD),Jack将它放入了模板的字符串部分。JavaScript理应由JavaScript引擎进行处理,字符串部分由Jack的 i18n标签进行处理。使用者能够经过i18n的文档了解到,:c(CAD)表明加拿大元的货币单位。
这就是标签模板的大部分实际应用了。
// 基于纯粹虚构的模板语言
// ES6标签模板。
var libraryHtml = hashTemplate`
<ul>
#for book in ${myBooks}
<li><i>#{book.title}</i> by #{book.author}</li>
#end
</ul>
`;
标签模板带来的灵活性远不止于此,要记住,标签函数的参数不会自动转换为字符串,它们如返回值同样,能够是任何值,标签模板甚至不必定要是字符串!你能够用自定义的标签来建立正则表达式、DOM树、图片、以promises为表明的整个异步过程、JS数据结构、GL着色器……
标签模板以开放的姿态欢迎库设计者们来建立强有力的领域特定语言。这些语言可能看起来不像JS,可是它们仍能够无缝嵌入到JS中并与JS的其它语言特性智能交互。我不知道这一特性将会带领咱们走向何方,但它蕴藏着无限的可能性,这令我感到异常兴奋!
在服务器端,io.js支持ES6的模板字符串。
在浏览器端,Firefox 34+支持模板字符串。它们由去年夏天的实习生项目组里的Guptha Rajagopal实现。模板字符串一样在Chrome 41+中得以支持,可是IE和Safari都不支持。到目前为止,若是你想要在web端使用模板字符串的功能,你将须要Babel或Traceur协助你完成ES6到ES5的代码转译,你也能够在TypeScript中当即使用这一特性。
嗯?
哦…这是个好问题。
(这一章节与JavaScript无关,若是你不使用Markdown,能够跳过这一章。)
对于模板字符串而言,Markdown和JavaScript如今都使用`字符来表示一些特殊的事物。事实上,在Markdown中,反撇号用来分割在内联文本中间的代码片断。
这会带来许多问题!若是你在Markdown中写这样的文档:
To display a message, write `alert(`hello world!`)`.
它将这样显示:
To display a message, write alert(hello world!).
请注意,输出文本中的反撇号消失了。Markdown将全部的四个反撇号解释为代码分隔符并用HTML标签将其替换掉。
为了不这样的状况发生,咱们要借助Markdown中的一个不为人知的特性,你能够使用多行反撇号做为代码分隔符,就像这样:
To display a message, write ``alert(`hello world!`)``.
在这个Gist有具体代码细节,它由Markdown写成,因此你能够直接查看源代码。
咱们一般使用可变参函数来构造API,可变参函数可接受任意数量的参数。例如,String.prototype.concat方法就能够接受任意数量的字符串参数。ES6提供了一种编写可变参函数的新方式——不定参数。
咱们经过一个简单的可变参数函数containsAll给你们演示不定参数的用法。函数containsAll能够检查一个字符串中是否包含若干个子串,例如:containsAll("banana", "b", "nan")返回true,containsAll("banana", "c", "nan")返回false。
首先使用传统方法来实现这个函数:
function containsAll(haystack) {
for (var i = 1; i < arguments.length; i++) {
var needle = arguments[i];
if (haystack.indexOf(needle) === -1) {
return false;
}
}
return true;
}
在这个实现中,咱们用到了神奇的arguments对象,它是一个类数组对象,其中包含了传递给函数的全部参数。这段代码实现了咱们的需求,但它的可读性却不是最理想的。函数的参数列表中只有一个参数 haystack,咱们没法一眼就看出这个函数实际上接受了多个参数。另外,咱们必定要注意,应该从1开始迭代,而不是从0开始,由于 arguments[0]至关于参数haystack。若是咱们想要在haystack先后添加另外一个参数,咱们必定要记得更新循环体。不定参数刚好能够解决可读性与参数索引的问题。下面是用ES6不定参数特性实现的containsAll函数:
function containsAll(haystack, ...needles) {
for (var needle of needles) {
if (haystack.indexOf(needle) === -1) {
return false;
}
}
return true;
}
这一版containsAll函数与前者有相同的行为,但这一版中使用了一个特殊的...needles语法。咱们来看一下调用 containsAll("banana", "b", "nan")以后的函数调用过程,与以前同样,传递进来的第一个参数"banana"赋值给参数haystack,needles前的省略号代表它是一个不定参数,全部传递进来的其它参数都被放到一个数组中,赋值给变量needles。对于咱们的调用示例而言,needles被赋值为["b", "nan"],后续的函数执行过程一如往常。(注意啦,咱们已经使用过ES6中for-of循环。)
在全部函数参数中,只有最后一个才能够被标记为不定参数。函数被调用时,不定参数前的全部参数都正常填充,任何“额外的”参数都被放进一个数组中并赋值给不定参数。若是没有额外的参数,不定参数就是一个空数组,它永远不会是undefined。
一般来讲,函数调用者不须要传递全部可能存在的参数,没有被传递的参数可由感知到的默认参数进行填充。JavaScript有严格的默认参数格式,未被传值的参数默认为undefined。ES6引入了一种新方式,能够指定任意参数的默认值。
下面是一个简单的示例(反撇号表示模板字符串,上周已经讨论过。):
function animalSentence(animals2="tigers", animals3="bears") {
return `Lions and ${animals2} and ${animals3}! Oh my!`;
}
默认参数的定义形式为[param1[ = defaultValue1 ][, ..., paramN[ = defaultValueN ]]],对于每一个参数而言,定义默认值时=后的部分是一个表达式,若是调用者没有传递相应参数,将使用该表达式的值做为参数默认值。相关示例以下:
animalSentence(); // Lions and tigers and bears! Oh my!
animalSentence("elephants"); // Lions and elephants and bears! Oh my!
animalSentence("elephants", "whales"); // Lions and elephants and whales! Oh my!
默认参数有几个微妙的细节须要注意:
默认值表达式在函数调用时自左向右求值,这一点与Python不一样。这也意味着,默认表达式能够使用该参数以前已经填充好的其它参数值。举个例子,咱们优化一下刚刚那个动物语句函数:
function animalSentenceFancy(animals2="tigers",
animals3=(animals2 == "bears") ? "sealions" : "bears")
{
return `Lions and ${animals2} and ${animals3}! Oh my!`;
}
如今,animalSentenceFancy("bears")将返回“Lions and bears and sealions. Oh my!”。
传递undefined值等效于不传值,因此animalSentence(undefined, "unicorns")将返回“Lions and tigers and unicorns! Oh my!”。
没有默认值的参数隐式默认为undefined,因此
function myFunc(a=42, b) {...}
是合法的,而且等效于
function myFunc(a=42, b=undefined) {...}
如今咱们已经看到了arguments对象可被不定参数和默认参数完美代替,移除arguments后一般会使代码更易于阅读。除了破坏可读性外,众所周知,针对arguments对象对JavaScript虚拟机进行的优化会致使一些让你头疼不已的问题。
咱们期待着不定参数和默认参数能够彻底取代arguments,要实现这个目标,标准中增长了相应的限制:在使用不定参数或默认参数的函数中禁止使用arguments对象。曾经实现过arguments的引擎不会当即移除对它的支持,固然,如今更推荐使用不定参数和默认参数。
Firefox早在第15版的时候就支持了不定参数和默认参数。
不幸的是,还没有有其它已发布的浏览器支持不定参数和默认参数。V8引擎最近增添了针对不定参数的实验性的支持,而且有一个开放状态的V8 issue给实现默认参数使用,JSC一样也有一个开放的issue来给不定参数和默认参数使用。
Babel和Traceur编译器都支持默认参数,因此从如今起就能够开始使用。
解构赋值容许你使用相似数组或对象字面量的语法将数组和对象的属性赋给各类变量。这种赋值语法极度简洁,同时还比传统的属性访问方法更为清晰。
一般来讲,你极可能这样访问数组中的前三个元素:
var first = someArray[0];
var second = someArray[1];
var third = someArray[2];
若是使用解构赋值的特性,将会使等效的代码变得更加简洁而且可读性更高:
var [first, second, third] = someArray;
SpiderMonkey(Firefox的JavaScript引擎)已经支持解构的大部分功能,可是仍不健全。你能够经过bug 694100跟踪解构和其它ES6特性在SpiderMonkey中的支持状况。
以上是数组解构赋值的一个简单示例,其语法的通常形式为:
[ variable1, variable2, ..., variableN ] = array;
这将为variable1到variableN的变量赋予数组中相应元素项的值。若是你想在赋值的同时声明变量,可在赋值语句前加入var
、let
或const
关键字,例如:
var [ variable1, variable2, ..., variableN ] = array;
let [ variable1, variable2, ..., variableN ] = array;
const [ variable1, variable2, ..., variableN ] = array;
事实上,用变量
来描述并不恰当,由于你能够对任意深度的嵌套数组进行解构:
var [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo);
// 1
console.log(bar);
// 2
console.log(baz);
// 3
此外,你能够在对应位留空来跳过被解构数组中的某些元素:
var [,,third] = ["foo", "bar", "baz"];
console.log(third);
// "baz"
并且你还能够经过“不定参数”模式捕获数组中的全部尾随元素:
var [head, ...tail] = [1, 2, 3, 4];
console.log(tail);
// [2, 3, 4]
当访问空数组或越界访问数组时,对其解构与对其索引的行为一致,最终获得的结果都是:undefined
。
console.log([][0]);
// undefined
var [missing] = [];
console.log(missing);
// undefined
请注意,数组解构赋值的模式一样适用于任意迭代器:
function* fibs() {
var a = 0;
var b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
var [first, second, third, fourth, fifth, sixth] = fibs();
console.log(sixth);
// 5
经过解构对象,你能够把它的每一个属性与不一样的变量绑定,首先指定被绑定的属性,而后紧跟一个要解构的变量。
var robotA = { name: "Bender" };
var robotB = { name: "Flexo" };
var { name: nameA } = robotA;
var { name: nameB } = robotB;
console.log(nameA);
// "Bender"
console.log(nameB);
// "Flexo"
当属性名与变量名一致时,能够经过一种实用的句法简写:
var { foo, bar } = { foo: "lorem", bar: "ipsum" };
console.log(foo);
// "lorem"
console.log(bar);
// "ipsum"
与数组解构同样,你能够随意嵌套并进一步组合对象解构:
var complicatedObj = {
arrayProp: [
"Zapp",
{ second: "Brannigan" }
]
};
var { arrayProp: [first, { second }] } = complicatedObj;
console.log(first);
// "Zapp"
console.log(second);
// "Brannigan"
当你解构一个未定义的属性时,获得的值为undefined
:
var { missing } = {};
console.log(missing);
// undefined
请注意,当你解构对象并赋值给变量时,若是你已经声明或不打算声明这些变量(亦即赋值语句前没有let
、const
或var
关键字),你应该注意这样一个潜在的语法错误:
{ blowUp } = { blowUp: 10 };
// Syntax error 语法错误
为何会出错?这是由于JavaScript语法通知解析引擎将任何以{开始的语句解析为一个块语句(例如,{console}
是一个合法块语句)。解决方案是将整个表达式用一对小括号包裹:
({ safe } = {});
// No errors 没有语法错误
当你尝试解构null
或undefined
时,你会获得一个类型错误:
var {blowUp} = null;
// TypeError: null has no properties(null没有属性)
然而,你能够解构其它原始类型,例如:布尔值
、数值
、字符串
,可是你将获得undefined
:
var {wtf} = NaN;
console.log(wtf);
// undefined
你可能对此感到意外,但通过进一步审查你就会发现,缘由其实很是简单。当使用对象赋值模式时,被解构的值须要被强制转换为对象。大多数类型均可以被转换为对象,但null
和undefined
却没法进行转换。当使用数组赋值模式时,被解构的值必定要包含一个迭代器。
当你要解构的属性未定义时你能够提供一个默认值:
var [missing = true] = [];
console.log(missing);
// true
var { message: msg = "Something went wrong" } = {};
console.log(msg);
// "Something went wrong"
var { x = 3 } = {};
console.log(x);
// 3
(译者按:Firefox目前只实现了这个特性的前两种状况,第三种还没有实现。详情查看bug 932080。)
做 为开发者,咱们须要实现设计良好的API,一般的作法是为函数为函数设计一个对象做为参数,而后将不一样的实际参数做为对象属性,以免让API使用者记住 多个参数的使用顺序。咱们能够使用解构特性来避免这种问题,当咱们想要引用它的其中一个属性时,大可没必要反复使用这种单一参数对象。
function removeBreakpoint({ url, line, column }) {
// ...
}
这是一段来自Firefox开发工具JavaScript调试器(一样使用JavaScript实现——没错,就是这样!)的代码片断,它看起来很是简洁,咱们会发现这种代码模式特别讨喜。
延伸一下以前的示例,咱们一样能够给须要解构的对象属性赋予默认值。当咱们构造一个提供配置的对象,而且须要这个对象的属性携带默认值时,解构特性就派上用场了。举个例子,jQuery的ajax
函数使用一个配置对象做为它的第二参数,咱们能够这样重写函数定义:
jQuery.ajax = function (url, {
async = true,
beforeSend = noop,
cache = true,
complete = noop,
crossDomain = false,
global = true,
// ... 更多配置
}) {
// ... do stuff
};
如此一来,咱们能够避免对配置对象的每一个属性都重复var foo = config.foo || theDefaultFoo;
这样的操做。
(编者按:不幸的是,对象的默认值简写语法仍未在Firefox中实现,我知道,上一个编者按后的几个段落讲解的就是这个特性。点击bug 932080查看最新详情。)
ECMAScript 6中定义了一个迭代器协议,咱们在《深刻浅出ES6(二):迭代器和for-of循环》中已经详细解析过。当你迭代Maps(ES6标准库中新加入的一种对象)后,你能够获得一系列形如[key, value]
的键值对,咱们可将这些键值对解构,更轻松地访问键和值:
var map = new Map();
map.set(window, "the global");
map.set(document, "the document");
for (var [key, value] of map) {
console.log(key + " is " + value);
}
// "[object Window] is the global"
// "[object HTMLDocument] is the document"
只遍历键:
for (var [key] of map) {
// ...
}
或只遍历值:
for (var [,value] of map) {
// ...
}
JavaScript语言中还没有整合多重返回值的特性,可是无须画蛇添足,由于你本身就能够返回一个数组并将结果解构:
function returnMultipleValues() {
return [1, 2];
}
var [foo, bar] = returnMultipleValues();
或者,你能够用一个对象做为容器并为返回值命名:
function returnMultipleValues() {
return {
foo: 1,
bar: 2
};
}
var { foo, bar } = returnMultipleValues();
这两个模式都比额外保存一个临时变量要好得多。
function returnMultipleValues() {
return {
foo: 1,
bar: 2
};
}
var temp = returnMultipleValues();
var foo = temp.foo;
var bar = temp.bar;
或者使用CPS变换:
function returnMultipleValues(k) {
k(1, 2);
}
returnMultipleValues((foo, bar) => ...);
你是否还没有使用ES6模块?还用着CommonJS的模块呢吧!没问题,当咱们导入CommonJS模块X时,极可能在模块X中导出了许多你根本没打算用的函数。经过解构,你能够显式定义模块的一部分来拆分使用,同时还不会污染你的命名空间:
const { SourceMapConsumer, SourceNode } = require("source-map");
(若是你使用ES6模块,你必定知道在import
声明中有一个类似的语法。)
正如你所见,解构在许多独立小场景中很是实用。在Mozilla咱们已经积累了许多有关解构的使用经验。十年前,Lars Hansen在Opera中引入了JS解构特性,Brendan Eich随后就给Firefox也增长了相应的支持,移植时版本为Firefox 2。因此咱们能够确定,渐渐地,你会在天天使用的语言中加入解构这个新特性,它可让你的代码变得更加精简整洁。
箭头符号在JavaScript诞生时就已经存在,当初第一个JavaScript教程曾建议在HTML注释内包裹行内脚本,这样能够避免不支持JS的浏览器误将JS代码显示为文本。你会写这样的代码:
<script language="javascript">
<!--
document.bgColor = "brown"; // red
// -->
</script>
老式浏览器会将这段代码解析为两个不支持的标签和一条注释,只有新式浏览器才能识别出其中的JS代码。
为了支持这种奇怪的hack方式,浏览器中的JavaScript引擎将<!--
这四个字符解析为单行注释的起始部分,我没开玩笑,这自始至终就是语言的一部分,直到如今仍然有效,这种注释符号不只出现<script>
标签后的首行,在JS代码的每一个角落你都有可能见到它,甚至在Node中也是如此。
碰巧,这种注释风格首次在ES6中被标准化了,但在新标准中箭头被用来作其它事情。
箭头序列 –—>
一样是单行注释的一部分。古怪的是,在HTML中-->
以前的字符是注释的一部分,而在JS中-->
以后的部分才是注释。
你必定感到陌生的是,只有当箭头在行首时才会注释当前行。这是由于在其它上下文中,-->
是一个JS运算符:“趋向于”运算符!
function countdown(n) {
while (n --> 0) // "n goes to zero"
alert(n);
blastoff();
}
上面这段代码能够正常运行,循环会一直重复直到n
趋于0,这固然不是ES6中的新特性,它只不过是将两个你早已熟悉的特性经过一些误导性的手段结合在一块儿。你能理解么?一般来讲,相似这种谜团均可以在Stack Overflow上找到答案。
固然,一样地,小于等于操做符<=
也形似箭头,你能够在JS代码、隐藏的图片样式中找到更多相似的箭头,可是咱们就不继续寻找了,你应该注意到咱们漏掉了一种特殊的箭头。
<!--
单行注释
-->
“趋向于”操做符
<=
小于等于
=>
这又是什么?
=>
究竟是什么?咱们今天就来一探究竟。
首先,咱们谈论一些有关函数的事情。
JavaScript中有一个有趣的特性,不管什么时候,当你须要一个函数时,你均可以在想添加的地方输入这个函数。
举个例子,假设你尝试告诉浏览器用户点击一个特定按钮后的行为,你会这样写:
$("#confetti-btn").click(
jQuery的.click()
方法接受一个参数:一个函数。没问题,你能够在这里输入一个函数:
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
对于如今的咱们来讲,写出这样的代码至关天然,而回忆起在这种编程方式流行以前,这种写法相对陌生一些,许多语言中都没有这种特性。1958年,Lisp首先支持函数表达式,也支持调用lambda函数,而C++,Python、C#以及Java在随后的多年中一直不支持这样的特性。
如今大相径庭,全部的四种语言都已支持lambda函数,更新出现的语言广泛都支持内建的lambda函数。咱们必需要感谢JavaScript和早期的JavaScript程序员,他们勇敢地构建了重度依赖lambda函数的库,让这种特性被普遍接受。
使人伤感的是,随后在全部我说起的语言中,只有JavaScript的lambda的语法最终变得冗长乏味。
// 六种语言中的简单函数示例
function (a) { return a > 0; } // JS
[](int a) { return a > 0; } // C++
(lambda (a) (> a 0)) ;; Lisp
lambda a: a > 0 # Python
a => a > 0 // C#
a -> a > 0 // Java
ES6中引入了一种编写函数的新语法
// ES5
var selected = allJobs.filter(function (job) {
return job.isSelected();
});
// ES6
var selected = allJobs.filter(job => job.isSelected());
当你只须要一个只有一个参数的简单函数时,能够使用新标准中的箭头函数,它的语法很是简单:标识符=>表达式
。你无需输入function
和return
,一些小括号、大括号以及分号也能够省略。
(我我的对于这个特性很是感激,再也不须要输入function
这几个字符对我而言相当重要,由于我老是不可避免地错误写成functoin
,而后我就不得不回过头改正它。)
若是要写一个接受多重参数(也可能没有参数,或者是不定参数、默认参数、参数解构)的函数,你须要用小括号包裹参数list。
// ES5
var total = values.reduce(function (a, b) {
return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);
我认为这看起来酷毙了。
正如你使用相似Underscore.js和Immutable.js这样的库提供的函数工具,箭头函数运行起来一样美不可言。事实上,Immutable的文档中的示例全都由ES6写成,其中的许多特性已经用上了箭头函数。
那么不是很是函数化的状况又如何呢?除表达式外,箭头函数还能够包含一个块语句。回想一下咱们以前的示例:
// ES5
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
这是它们在ES6中看起来的样子:
// ES6
$("#confetti-btn").click(event => {
playTrumpet();
fireConfettiCannon();
});
这是一个微小的改进,对于使用了Promises的代码来讲箭头函数的效果能够变得更加戏剧性,}).then(function (result) {
这样的一行代码能够堆积起来。
注意,使用了块语句的箭头函数不会自动返回值,你须要使用return
语句将所需值返回。
小提示:当使用箭头函数建立普通对象时,你老是须要将对象包裹在小括号里。
// 为与你玩耍的每个小狗建立一个新的空对象
var chewToys = puppies.map(puppy => {}); // 这样写会报Bug!
var chewToys = puppies.map(puppy => ({})); //
用小括号包裹空对象就能够了。
不幸的是,一个空对象{}
和一个空的块{}
看起来彻底同样。ES6中的规则是,紧随箭头的{被解析为块的开始,而不是对象的开始。所以,puppy => {}
这段代码就被解析为没有任何行为并返回undefined
的箭头函数。
更使人困惑的是,你的JavaScript引擎会将相似{key: value}
的对象字面量解析为一个包含标记语句的块。幸运的是,{
是惟一一个有歧义的字符,因此用小括号包裹对象字面量是惟一一个你须要牢记的小窍门。
普通function
函数和箭头函数的行为有一个微妙的区别,箭头函数没有它本身的this
值,箭头函数内的this
值继承自外围做用域。
在咱们尝试说明这个问题前,先一块儿回顾一下。
JavaScript中的this
是如何工做的?它的值从哪里获取?这些问题的答案可都不简单,若是你对此倍感清晰,必定由于你长时间以来一直在处理相似的问题。
这个问题常常出现的其中一个缘由是,不管是否须要,function
函数总会自动接收一个this
值。你是否写过这样的hack代码:
{
...
addAll: function addAll(pieces) {
var self = this;
_.each(pieces, function (piece) {
self.add(piece);
});
},
...
}
在这里,你但愿在内层函数里写的是this.add(piece)
,不幸的是,内层函数并未从外层函数继承this
的值。在内层函数里,this
会是window
或undefined
,临时变量self
用来将外部的this
值导入内部函数。(另外一种方式是在内部函数上执行.bind(this)
,两种方法都不甚美观。)
在ES6中,不须要再hackthis
了,但你须要遵循如下规则:
object.method()
语法调用的方法使用非箭头函数定义,这些函数须要从调用者的做用域中获取一个有意义的this
值。// ES6
{
...
addAll: function addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
在ES6的版本中,注意addAll
方法从它的调用者处获取了this
值,内部函数是一个箭头函数,因此它继承了外围做用域的this
值。
超赞的是,在ES6中你能够用更简洁的方式编写对象字面量中的方法,因此上面这段代码能够简化成:
// ES6的方法语法
{
...
addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
在方法和箭头函数之间,我不再会错写functoin
了,这真是一个绝妙的设计思想!
箭头函数与非箭头函数间还有一个细微的区别,箭头函数不会获取它们本身的arguments
对象。诚然,在ES6中,你可能更多地会使用不定参数和默认参数值这些新特性。
咱们已经讨论了许多箭头函数的实际用例,它还有一种可能的使用方法:将ES6箭头函数做为一个学习工具,来深刻挖掘计算的本质,是否实用,终将取决于你本身。
1936年,Alonzo Church和Alan Turing各自开发了强大的计算数学模型,图灵将他的模型称为a-machines,可是每个人都称其为图灵机。Church写的是函数模型,他的模型被称为lambda演算(λ-calculus)。这一成果也被Lisp借鉴,用LAMBDA
来指示函数,这也是为什么咱们如今将函数表达式称为lambda函数。
用几句话解释清楚很难,可是我会努力阐释:lambda演算是第一代编程语言的一种形式,但毕竟存储程序计算机在十几二十年后才诞生,因此它本来不是为编程语言设计的,而是为了表达任意你想到的计算问题设计的一种极度简化的纯数学思想的语言。Church但愿用这个模型来证实广泛意义的计算。
最终他发现,在他的系统中只须要一件东西:函数。
这种声明方式无与伦比,不借助对象、数组、数字、if
语句、while
循环、分号、赋值、逻辑运算符甚或是事件循环,只须使用函数就能够从0开始重建JavaScript能实现的每一种计算。
这是用Church的lambda标记写出来的数学家风格的“程序”示例:
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
等效的JavaScript函数是这样的:
var fix = f => (x => f(v => x(x)(v)))
(x => f(v => x(x)(v)));
因此,在JavaScript中实现了一个能够运行的lambda演算,它根植于这门语言中。
Alonzo Church和lambda演算后继研究者们的故事,以及它是如何潜移默化地入驻每一门主流编程语言的,已经远超本文的讨论范围。可是若是你对计算机科学 的奠定感兴趣,或者你只是对一门只用函数就能够作许多相似循环和递归这样的事情的语言倍感兴趣,你能够在一个下雨的午后深刻邱奇数(Church numerals)和不动点组合子(Fixed-point combinator),在你的Firefox控制台或Scratchpad中仔细研究一番。结合ES6的箭头函数以及其它强大的功能,JavaScript称得上是一门探索lambda演算的最好的语言。
早在2013年,我就在Firefox中实现了ES6箭头函数的功能,Jan de Mooij为其优化加快了执行速度。感谢Tooru Fujisawa以及ziyunfei(译者注:中国开发者,为Mozilla做了许多贡献)后续打的补丁。
微软Edge预览版中也实现了箭头函数的功能,若是你想当即在你的Web项目中使用箭头函数,能够使用Babel、Traceur或TypeScript,这三个工具均已实现相关功能。
你是否知道ES6中的Symbols是什么,它有什么做用呢?我相信你极可能不知道,那就让咱们一探究竟!
Symbols并不是用来指代某种Logo。
它们也不是能够用做代码的小图标。
它们不是代替其它东西的文学手法。
它们更不可能被用来指代谐音词Cymbals(铙钹)。
(编程的时候最好不要演奏铙钹,它们太过吵闹,极可能致使你的程序崩溃。)
那么,Symbols究竟是什么呢?
1997年JavaScript首次被标准化,那时只有六种原始类型,在ES6之前,JS程序中使用的每个值都是如下几种类型之一:
每种类型都是多个值的集合,前五个集合是有限的。布尔类型只有两个值,true
和false
,不会再创造第三种布尔值;数字类型和字符串类型的值更多,标准指明一共有18,437,736,874,454,810,627种不一样的数字(包括NaN
, 亦即“Not a Number”的缩写,表明非数字),可能存在的字符串类型的值拥有无以匹敌的数量,我估算了一下大约是 (2144,115,188,075,855,872 − 1) ÷ 65,535种……固然,我极可能得出了一个错误的答案,但字符串类型值的集合必定是有限的。
然而,对象类型值的集合是无限的。每个对象都像珍贵的雪花同样独一无二,每一次你打开一个Web页面,都会建立一堆对象。
ES6新特性中的symbol也是值,但它不是字符串,也不是对象,而是是全新的——第七种类型的原始值。
让咱们一块儿探讨一下symbol的实际应用场景。
有时候你能够很是轻松地将别人的外部数据存储到一个JavaScript对象中。
举 个例子,假设你正在写一个JS库,能够经过CSS transitions使DOM元素在屏幕上移动。你可能会注意到,当你尝试在一个div元素上同时应用多重CSS transitions时并不会生效。实际效果是丑陋而又不连续的“跳闪”。你认为能够修复这个问题,但前提是你须要一种发现给定元素是否已经移动过的方 法。
应当如何解决这个问题呢?
一种方法是,用CSS API来告诉浏览器元素是否正在移动,但这样简直小题大作。在元素移动的第一时间内你的库就应该记录下移动的状态,因此它天然知道元素正在移动。
你真正想要的是一种持续跟踪某个元素正在移动的方法。你能够维护一个数组,记录全部正在移动的元素,每当你的库被调用来移动某个元素时,你能够检索数组来查看元素是否已经存在,亦即它是否正在移动中。
固然,若是数组很是大的话,线性搜索将会很是缓慢。
实际上你只想为元素设置一个标记:
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
这样也会有一些潜在的问题,事实上,你的代码极可能不是惟一一段操做DOM的代码。
for-in
或Object.keys()
的代码。固然你能够选择一个乏味而愚蠢的命名(其余人根本不会想用的那些名称)来解决最后的三个问题:
if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) {
smoothAnimations(element);
}
element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true;
这只会形成无畏的眼疲劳。
借助于密码学,你能够生成一个惟一的属性名称:
// 获取1024个Unicode字符的无心义命名
var isMoving = SecureRandom.generateName();
...
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
object[name]
语法容许你使用几乎任何字符串做为属性名称。因此这个方法行之有效:冲突几乎是不可能的,而且你的代码看起来也很简洁。
可是这也将带来不良的调试体验。每当你在控制台输出(console.log()
)包含那个属性的元素时,你将会看到一堆巨大的字符串垃圾。假使你须要比这多得多的相似属性呢?你如何保持它们整齐划一?每当你重载的时候它们的命名甚至都不同!
为何这个问题如此困难?咱们只想要一个小小的布尔值啊!
symbol是程序建立而且能够用做属性键的值,而且它能避免命名冲突的风险。
var mySymbol = Symbol();
调用Symbol()
建立一个新的symbol,它的值与其它任何值皆不相等。
字符串或数字能够做为属性的键,symbol也能够,它不等同于任何字符串,于是这个以symbol为键的属性能够保证不与任何其它属性产生冲突。
obj[mySymbol] = "ok!"; // 保证不会冲突
console.log(obj[mySymbol]); // ok!
想要在上述讨论的场景中使用symbol,你能够这样作:
// 建立一个独一无二的symbol
var isMoving = Symbol("isMoving");
...
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
有关这段代码的一些解释:
Symbol("isMoving")
中的isMoving
被称做描述。你能够经过console.log()
将它打印出来,对调试很是有帮助;你也能够用.toString()
方法将它转换为字符串呈现;它也能够被用在错误信息中。
element[isMoving]
被称做一个以symbol为键(symbol-keyed)的属性。简而言之,它的名字是symbol
而不是一个字符串。除此以外,它与一个普通的属性没有什么区别。
以symbol为键的属性属性与数组元素相似,不能被相似obj.name
的点号法访问,你必须使用方括号访问这些属性。
若是你已经获得了symbol,那么访问一个以symbol为键的属性一样简单,以上的示例很好地展现了如何获取element[isMoving]
的值以及如何为它赋值。若是咱们须要,能够查看属性是否存在:if (isMoving in element)
,也能够删除属性:delete element[isMoving]
。
另外一方面,只有当isMoving
在当前做用域中时才会生效。这是symbol的弱封装机制:模块建立了几个symbol,能够在任意对象上使用,无须担忧与其它代码建立的属性产生冲突。
symbol键的设计初衷是避免初衷,所以JavaScript中最多见的对象检查的特性会忽略symbol键。例如,for-in
循环只会遍历对象的字符串键,symbol键直接跳过,Object.keys(obj)
和Object.getOwnPropertyNames(obj)
也是同样。可是symbols也不彻底是私有的:用新的API Object.getOwnPropertySymbols(obj)
就能够列出对象的symbol键。另外一个新的API,Reflect.ownKeys(obj)
,会同时返回字符串键和symbol键。(咱们将在随后的文章中讲解Reflect(反射) API)。
慢慢地咱们会发现,愈来愈多的库和框架将大量使用symbol,语言自己也会将symbol应用于普遍的用途。
> typeof Symbol()
"symbol"
确切地说,symbol与其它类型并不彻底相像。
symbol被建立后就不可变动,你不能为它设置属性(在严格模式下尝试设置属性会获得TypeError的错误)。他们能够用做属性名称,这些性质与字符串相似。
另外一方面,每个symbol都独一无二,不与其它symbol等同,即便两者有相同的描述也不相等;你能够轻松地建立一个新的symbol。这些性质与对象相似。
ES6中的symbol与Lisp和Ruby这些语言中更传统的symbol相似,但不像它们集成得那么紧密。在Lisp中,全部的标识符都是symbol;在JS中,标识符和大多数的属性键仍然是字符串,symbol只是一个额外的选项。
关于symbol的忠告:symbol不能被自动转换为字符串,这和语言中的其它类型不一样。尝试拼接symbol与字符串将获得TypeError错误。
> var sym = Symbol("<3");
> "your symbol is " + sym
// TypeError: can't convert symbol to string
> `your symbol is ${sym}`
// TypeError: can't convert symbol to string
经过String(sym)
或sym.toString()
能够显示地将symbol转换为一个字符串,从而回避这个问题。
有三种获取symbol的方法。
调用Symbol()。正如咱们上文中所讨论的,这种方式每次调用都会返回一个新的惟一symbol。
调用Symbol.for(string)。这种方式会访问symbol注册表,其中存储了已经存在的一系列symbol。这种方式与经过Symbol()
定义的独立symbol不一样,symbol注册表中的symbol是共享的。若是你连续三十次调用Symbol.for("cat")
,每次都会返回相同的symbol。注册表很是有用,在多个web页面或同一个web页面的多个模块中常常须要共享一个symbol。
使用标准定义的symbol,例如:Symbol.iterator。标准根据一些特殊用途定义了少量的几个symbol。
若是你尚不肯定symbol是否实用,最后这一章将向你展现symbol在实际应用中发挥的巨大做用,很是有趣!
在以前的文章《深刻浅出ES6(二):迭代器和for-of循环》中,咱们已经领略了借助ES6 symbol的力量避免代码冲突的方法,循环for (var item of myArray)
首先调用myArray[Symbol.iterator]()
,当时我提到这种写法是为了替代myArray.iterator()
,拥有更好的向后兼容性。
如今咱们知道symbol究竟是什么了,天然很容易理解为何咱们要创造一个symbol以及它为咱们带来什么新特性。
ES6中还有其它几处使用了symbol的地方。(这些特性在Firefox里还没有实现。)
使instanceof可扩展。在ES6中,表达式object instanceof constructor
被指定为构造函数的一个方法:constructor[Symbol.hasInstance](object)
。这意味着它是可扩展的。
消除新特性和旧代码之间的冲突。这一点很是复杂,可是咱们发现,添加某些ES6数组方法会破坏现有的Web网站。其它Web标准有相同的问题:向浏览器中添加新方法会破坏原有的网站。然而,破坏问题主要由动态做用域引发,因此ES6引入一个特殊的symbol——Symbol.unscopables
,Web标准能够用这个symbol来阻止某些方法别加入到动态做用域中。
支持新的字符串匹配类型。在ES5中,str.match(myObject)
会尝试将myObject
转换为正则表达式对象(RegExp
)。在ES6中,它会首先检查myObject
是否有一个myObject[Symbol.match](str)
方法。如今的库能够提供自定义的字符串解析类,全部支持RegExp
对象的环境均可以正常运行。
这些用例的应用范围都很是小,很难看到这些特性经过它们自身影响咱们每日的代码,长期来看才能体现它们的价值。实际上,symbol是PHP和Python中的__doubleUnderscores
在JavaScript语言环境中的改进版。标准将借助symbol的力量在将来向语言中添加新的钩子,同时无风险地将新特性添加到你已有的代码中。
symbol在Firefox 36和Chrome 38中均已被实现。Firefox中的实现由我亲自完成,因此若是你的symbol像铙钹(cymbals)同样行为异常,请直接联系我!
为了支持那些还没有支持原生ES6 symbol的浏览器,你能够使用一个polyfill,例如core.js。由于symbol与其它类型不尽相同,因此polyfill目前不是很完美。请阅读注意事项。
前段时间,官方名为“ECMA-262,第六版,ECMAScript 2015语言规范”的ES6规范终于结束了最后的征途,正式被承认为新的ECMA标准。让咱们祝贺TC39等全部做出贡献人们,ES6终于定稿了!
更好的消息是,下次更新不须要再等六年了。委员会如今努力要求,大约每12个月完成一个新的版本。第七版提议已经开始。
如今是时候庆祝庆祝了,让咱们来讨论一些好久以来我一直但愿在JS里看到的东西——固然,它们之后仍然有改进的余地。
JS和其它编程语言有些特殊的差异,有时,它们会以使人惊奇的方式影响到这门语言的发展。
ES6模块就是个很好的例子。其它语言的模块化系统中,Racket作得特别棒,Python也很好。那么,当标准委员会决定在ES6中增长模块时,为何他们不直接仿照一套已经存在的系统呢?
由于JS是不一样的,由于它要在浏览器里运行。读取和写入均可能花费较长时间,因此,JS须要一套支持异步加载代码的模块化系统,同时,也不能容许在文件夹中挨个搜索,照搬已有的系统并不能解决问题。ES6的模块化系统须要一些新技术。
讨论这些问题对最终设计的影响,会是个有趣的故事,不过咱们今天要讨论的并非模块。
这篇文章是关于ES6标准中所谓“键值集合”的:Set
,Map
,WeakSet
和WeakMap
。它们在大多数方面和其它语言中的哈希表同样,不过,正由于JS是不一样的,标准委员会在其中作了些有趣的权衡与调整。
熟悉JS必定会知道,咱们已经有了一种相似哈希表的东西:对象(Object
)。
一个普通的对象毕竟就只是一个开放的键值对集合。你能够进行获取、设置、删除、遍历——任何一个哈希表支持的操做。因此咱们到底为何要增长新的特性?
好吧,大多数程序简单地用对象来存储键值对就够了,对它们而言,没什么必要换用Map
或Set
。可是,直接这样使用对象有一些广为人知的问题:
Object.create(null)
而非直接写{}
,要么得当心地避免把Object.prototype.toString
之类的内置方法名做为键名来存储数据。Symbol
)而不能是另外一个对象。ES6中又出现了新问题:纯粹的对象不可遍历,也就是,它们不能配合for-of
循环或...
操做符等语法。
嗯,确实不少程序里这些问题都不重要,直接用纯对象仍然是正确的选择。Map
和Set
是为其它场合准备的。
这些ES6中的集合原本就是为避免用户数据与内置方法冲突而设计的,因此它们不会把数据做为属性暴露出来。也就是说,obj.key
或obj[key]
不能再用来访问数据了,取而代之的是map.get(key)
。同时,不像属性,哈希表的键值不能经过原型链来继承了。
好消息是,不像纯粹的Object
,Map
和Set
有本身的方法了,而且,更多标准或自定义的方法能够无需担忧冲突地加入。
一个Set
是一群值的集合。它是可变的,可以增删元素。如今,还没说到它和数组的区别,不过它们的区别就和类似点同样多。
首先,和数组不一样,一个Set
不会包含相同元素。试图再次加入一个已有元素不会产生任何效果。
这个例子里元素都是字符串,不过Set
是能够包含JS中任何类型的值的。一样,重复加入已有元素不会产生效果。
其次,Set
的数据存储结构专门为一种操做做了速度优化:包含性检测。
> // 检查"zythum"是否是一个单词
> arrayOfWords.indexOf("zythum") !== -1 // 慢
true
> setOfWords.has("zythum") // 快
true
Set
不能提供的则是索引。
> arrayOfWords[15000]
"anapanapa"
> setOfWords[15000] // Set不支持索引
undefined
如下是Set
支持的全部操做:
new Set
:建立一个新的、空的Set
。new Set(iterable)
:从任何可遍历数据中提取元素,构造出一个新的集合。set.size
:获取集合的大小,即其中元素的个数。set.has(value)
:断定集合中是否含有指定元素,返回一个布尔值。set.add(value)
:添加元素。若是与已有重复,则不产生效果。set.delete(value)
:删除元素。若是并不存在,则不产生效果。.add()
和.delete()
都会返回集合自身,因此咱们能够用链式语法。set[Symbol.iterator]()
:返回一个新的遍历整个集合的迭代器。通常这个方法不会被直接调用,由于实际上就是它使集合可以被遍历,也就是说,咱们能够直接写for (v of set) {...}
等等。set.forEach(f)
:直接用代码来解释好了,它就像是for (let value of set) { f(value, value, set); }
的简写,相似于数组的.forEach()
方法。set.clear()
:清空集合。set.keys()
、set.values()
和set.entries()
返回各类迭代器,它们是为了兼容Map
而提供的,因此咱们待会儿再来看。在这些特性中,负责构造集合的new Set(iterable)
是惟一一个在整个数据结构层面上操做的。你能够用它把数组转化为集合,在一行代码内去重;也能够传递一个生成器,函数会逐个遍历它,并把生成的值收录为一个集合;也能够用来复制一个已有的集合。
上周我答应过要给ES6中的新集合们挑挑刺,就从这里开始吧。尽管Set
已经很不错了,仍是有些被遗漏的方法,说不定补充到未来某个标准里会挺不错:
.map()
、.filter()
、.some()
和.every()
。set1.union(set2)
和set1.intersection(set2)
。set.addAll(iterable)
、set.removeAll(iterable)
和set.hasAll(iterable)
。好消息是,这些均可以用ES6已经提供了的方法来实现。
一个Map
对象由若干键值对组成,支持:
new Map
:返回一个新的、空的Map
。new Map(pairs)
:根据所含元素形如[key, value]
的数组pairs
来建立一个新的Map
。这里提供的pairs
能够是一个已有的Map
对象,能够是一个由二元数组组成的数组,也能够是逐个生成二元数组的一个生成器,等等。map.size
:返回Map
中项目的个数。map.has(key)
:测试一个键名是否存在,相似key in obj
。map.get(key)
:返回一个键名对应的值,若键名不存在则返回undefined
,相似obj[key]
。map.set(key, value)
:添加一对新的键值对,若是键名已存在就覆盖。map.delete(key)
:按键名删除一项,相似delete obj[key]
。map.clear()
:清空Map
。map[Symbol.iterator]()
:返回遍历全部项的迭代器,每项用一个键和值组成的二元数组表示。map.forEach(f)
相似for (let [key, value] of map) { f(value, key, map); }
。这里诡异的参数顺序,和Set
中同样,是对应着Array.prototype.forEach()
。map.keys()
:返回遍历全部键的迭代器。map.values()
:返回遍历全部值的迭代器。map.entries()
:返回遍历全部项的迭代器,就像map[Symbol.iterator]()
。实际上,它们就是同一个方法,不一样名字。还有什么要抱怨的?如下是我以为会有用而ES6还没提供的特性:
collections.defaultdict
。Map.fromObject(obj)
的辅助函数,以便更方便地用构造对象的语法来写出一个Map
。一样,这些特性也是很容易加上的。
到这里,还记不记得,开篇时我提到过运行于浏览器对语言特性设计的特殊影响?如今要好好谈一谈这个问题了。我已经有了三个例子,如下是前两个。
到目前为止,据我所知,ES6的集合类彻底不支持下述这种有用的特性。
好比说,咱们有若干 URL 对象组成的Set:
var urls = new Set;
urls.add(new URL(location.href)); // 两个 URL 对象。
urls.add(new URL(location.href)); // 它们同样么?
alert(urls.size); // 2
这两个 URL 应该按相同处理,毕竟它们有彻底同样的属性。但在JavaScript中,它们是各自独立、互不相同的,而且,绝对没有办法来重载相等运算符。
其它一些语言就支持这一特性。在Java, Python, Ruby中,每一个类均可以重载它的相等运算符;Scheme的许多实现中,每一个哈希表能够使用不一样的相等关系。C++则二者都支持。
可是,全部这些机制都须要编写者自行实现一个哈希函数并暴露出系统默认的哈希函数。在JS中,由于不得不考虑其它语言没必要担忧的互用性和安全性,委员会选择了不暴露——至少目前仍如此。
你多半以为一台计算机具备肯定性行为是理所应当的,但当我告诉别人遍历Map或Set的顺序就是其中元素的插入顺序时,他们老是很惊奇。没错,它就是肯定的。
咱们已经习惯了哈希表某些方面任性的行为,咱们学会了接受它。不过,总有一些足够好的理由让咱们但愿尝试避免这种不肯定性。2012年我写过:
在2012年2月以上种种意见被提出时,我是支持不肯定遍历序的。而后,我决定用实验证实,保存插入序将过分下降哈希表的效率。我写了一个C++的小型基准测试,结果却令我惊奇地偏偏相反。
这就是咱们最终为JS设计了按插入序遍历的哈希表的过程。
上篇文章咱们讨论了一个JS动画库相关的例子。咱们试着要为每一个DOM对象设置一个布尔值类型的标识属性,就像这样:
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
不幸的是,这样给一个DOM对象增长属性不是个好主意。缘由咱们上次已经解释过了。
上次的文章里,咱们接着展现了用Symbol解决这个问题的方法。可是,能够用集合来实现一样的效果么?也许看上去会像这样:
if (movingSet.has(element)) {
smoothAnimations(element);
}
movingSet.add(element);
这只有一个坏处。Map和Set都为内部的每一个键或值保持了强引用,也就是说,若是一个DOM元素被移除了,回收机制没法取回它占用的内存,除非movingSet
中也删除了它。在最理想的状况下,库在善后工做上对使用者都有复杂的要求,因此,这极可能引起内存泄露。
ES6给了咱们一个惊喜的解决方案:用WeakSet
而非Set
。和内存泄露说再见吧!
也 就是说,这个特定情景下的问题能够用弱集合(weak collection)或Symbol两种方法解决。哪一个更好呢?不幸的是,完整地讨论利弊取舍会把这篇文章拖得有些长。简而言之,若是能在整个网页的生 命周期内使用同一个Symbol,那就没什么问题;若是不得不使用一堆临时的Symbol,那就危险了,是时候考虑WeakMap来避免内存泄露了。
WeakMap和WeakSet被设计来完成与Map、Set几乎同样的行为,除了如下一些限制:
还要注意,这两种弱集合都不可迭代,除非专门查询或给出你感兴趣的键,不然不能得到一个弱集合中的项。
这些当心设计的限制让垃圾回收机制能回收仍在使用中的弱集合里的无效对象。这效果相似于弱引用或弱键字典,但ES6的弱集合能够在不暴露脚本中正在垃圾回收的前提下获得垃圾回收的效益。
弱集合其实是用 ephemeron 表实现的。
简单说,一个WeakSet并不对其中对象保持强引用。当WeakSet中的一个对象被回收时,它会简单地被从WeakSet中移除。WeakMap也相似地不为它的键保持强引用。若是一个键仍被使用,相应的值也就仍被使用。
为何要接受这些限制呢?为何不直接在JS中引入弱引用呢?
再 次地,这是由于标准委员会很不肯意向脚本暴露未定义行为。孱弱的跨浏览器兼容性是互联网发展的痛苦之源。弱引用暴露了底层垃圾回收的实现细节——这正是与 平台相关的一个未定义行为。应用固然不该该依赖平台相关的细节,但弱引用使咱们难于精确了解本身对测试使用的浏览器的依赖程度。这是件很不讲道理的事情。
相比之下,ES6的弱集合只包含了一套有限的特性,但它们至关牢靠。一个键或值被回收从不会被观测到,因此应用将不会依赖于其行为,即便只是缘于意外。
这是针对互联网的特殊考量引起了一个惊人的设计、进而使JS成为一门更好语言的一个例子。
总计四种集合类在Firefox、Chrome、Microsoft Edge、Safari中都已实现,要支持旧浏览器则须要 ES6 - Collections 之类来补全。
Firefox中的WeakMap 最初由 Andreas Gal 实现,他后来当了一段时间Mozilla的CTO。Tom Schuster实现了WeakSet,我实现了Map和Set。感谢Tooru Fujisawa贡献的几个相关补丁。
自ES6正式发布,人们已经开始讨论ES7:将来版本会保留哪些特性,新标准可能提供什么样的新特性。做为Web开发者,咱们想知道如何发挥这一切的巨大能量。在深刻浅出ES6系列以前的文章中,咱们不断鼓励你开始在编码中加入ES6新特性,辅以一些有趣的工具,你彻底能够从如今开始使用ES6:
若是你想在Web端使用这种新语法,你能够经过Babel或Google的Traceur将你的ES6代码转译为Web友好的ES5代码。
如今,咱们将向你分步展现如何作到的这一切。上面说起的工具被称为转译器,你能够将它理解为源代码到源代码的编译器——一个在可比较的抽象层上操做不一样编程语言相互转换的编译器。转译器容许咱们用ES6编写代码,同时保证这些代码能在每个浏览器中执行。
转译器使用起来很是简单,只需两步便可描述它所作的事情:
1,用ES6的语法编写代码。
let q = 99;
let myVariable = `${q} bottles of beer on the wall, ${q} bottles of beer.`;
2,用上面那段代码做为转译器的输入,通过处理后获得如下这段输出:
"use strict";
var q = 99;
var myVariable = "" + q + " bottles of beer on the wall, " + q + " bottles of beer."
这正是咱们熟知的老式JavaScript,这段代码能够在任意浏览器中运行。
转译器内部从输入到输出的逻辑高度复杂,彻底超出本篇文章的讲解范围。正如咱们无须知道全部的内部引擎结构就能够驾驶一辆汽车,如今,咱们一样能够将转译器视为一个可以处理咱们代码的黑盒。
你能够经过几种不一样的方法在项目中使用Babel,有一个命令行工具,在这个工具中能够使用以下形式的指令:
babel script.js --out-file script-compiled.js
Babel也提供支持在浏览器中使用的版本。你能够将Babel做为一个普通的库引入,而后将你的ES6代码放置在类型为text/babel
的script标签中。
<script src="node_modules/babel-core/browser.js"></script>
<script type="text/babel">
// 你的ES6代码
</script>
随着代码库爆炸式增加,你开始将全部代码划分为多个文件和文件夹,可是这些方法并不能随之扩展。到那时,你将须要一个构建工具以及一种将Babel与构建管道整合在一块儿的方法。
在接下来的章节中,咱们将要把Babel整合到构建工具Broccoli.js中,咱们将在两个示例中编写并执行第一行ES6代码。若是你的代码没法正常运行,能够在这里(broccoli-babel-examples)查看完整的源代码。在这个仓库中你能够找到三个示例项目:
每个项目都构建于前一个示例的基础之上,咱们会从最小的项目开始,逐步得出一个通常的解决方案,为往后每个雄心壮志的项目打下良好的开端。这篇文章只包含前两个示例,阅读文章后,你彻底能够自行阅读第三个示例中的代码并加以理解。
若是你在想——我坐等浏览器支持这些新特性就行了啦——那么你必定会落后的!实现全部功能要花费很长时间,何况如今有成熟的转译器,并且 ECMAScript加快了发布新版本的周期(每一年一版),咱们将会看到新标准比统一的浏览器平台更新得更频繁。因此赶快加入咱们,一块儿发挥新特性的巨大威力吧!
Broccoli是一个用来快速构建项目的工具,你能够用它对文件进行混淆与压缩,还能够经过众多的Broccoli插件实现许多其它功能。它帮助咱们处理文件和目录,每当项目变动时自动执行指令,很大程度上减轻了咱们的负担。你不妨将它视为:
相似Rails的asset管道,可是Broccoli运行在Node上且能够对接任意后端。
你可能已经猜到了,你须要安装Node 0.11或更高版本。
若是你使用unix系统,不要从包管理器(apt、yum等)中安装,这样能够避免在安装过程当中使用root权限,最好使用当前的用户权限,经过上面的连接手动安装。在文章《不要sudo npm》中能够了解为何不推荐使用root权限,文章中也给出了其它安装方案。
首先,咱们要配置好Broccoli项目:
mkdir es6-fruits
cd es6-fruits
npm init
# 建立一个名为Brocfile.js的空文件
touch Brocfile.js
如今咱们安装broccoli
和broccoli-cli
# 安装broccoli库
npm install --save-dev broccoli
# 命令行工具
npm install -g broccoli-cli
建立src文件夹,在里面置入fruits.js
文件。
mkdir src vim src/fruits.js
用ES6语法在新文件中写一小段脚本。
let fruits = [
{id: 100, name: '草莓'},
{id: 101, name: '柚子'},
{id: 102, name: '李子'}
];
for (let fruit of fruits) {
let message = `ID: ${fruit.id} Name: ${fruit.name}`;
console.log(message);
}
console.log(`List total: ${fruits.length}`);
上面的代码示例使用了三个ES6特性:
保存文件,尝试执行脚本。
node src/fruits.js
目前这段代码不能正常运行,可是咱们将会让它运行在Node与任何浏览器中。
let fruits = [
^^^^^^
SyntaxError: Unexpected identifier
如今,咱们用Broccoli加载代码,而后用Babel处理它。编辑Brocfile.js
文件并加入如下这段代码:
// 引入babel插件
var babel = require('broccoli-babel-transpiler');
// 获取源代码,执行转译指令(仅需1步)
fruits = babel('src'); // src/*.js
module.exports = fruits;
注意咱们引入了包裹在Babel库中的Broccoli插件broccoli-babel-transpiler
,因此咱们必定要安装它:
npm install --save-dev broccoli-babel-transpiler
如今咱们能够构建项目并执行脚本了:
broccoli build dist # 编译
node dist/fruits.js # 执行ES5
输出结果看起来应当是这样的:
ID: 100 Name: 草莓
ID: 101 Name: 柚子
ID: 102 Name: 李子
List total: 3
那很简单!你能够打开dist/fruits.js
查看转译后代码。Babel转译器的一个优秀特性是它可以生产可读的代码。
在第二个示例中,咱们将作进一步提高。首先,退出es6-fruits
文件夹,而后使用上述配置项目一章中列出的步骤建立新目录es6-website
。
在src文件夹中建立三个文件:src/index.html
<!DOCTYPE html>
<html>
<head>
<title>立刻使用ES6</title>
</head>
<style>
body {
border: 2px solid #9a9a9a;
border-radius: 10px;
padding: 6px;
font-family: monospace;
text-align: center;
}
.color {
padding: 1rem;
color: #fff;
}
</style>
<body>
<h1>立刻使用ES6</h1>
<div id="info"></div>
<hr>
<div id="content"></div>
<script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="js/my-app.js"></script>
</body>
</html>src/print-info.js
function printInfo() {
$('#info')
.append('<p>用Broccoli和Babel构建的' +
'最小网站示例</p>');
}
$(printInfo);src/print-colors.js
// ES6生成器
function* hexRange(start, stop, step) {
for (var i = start; i < stop; i += step) {
yield i;
}
}
function printColors() {
var content$ = $('#content');
// 人为的示例
for ( var hex of hexRange(900, 999, 10) ) {
var newDiv = $('<div>')
.attr('class', 'color')
.css({ 'background-color': `#${hex}` })
.append(`hex code: #${hex}`);
content$.append(newDiv);
}
}
$(printColors);
你可能已经注意到function* hexRange
,是的,那是ES6的生成器。这个特性目前还没有被全部浏览器支持。为了可以使用这个特性,咱们须要一个polyfill,Babel中已经支持,咱们很快将投入使用。
下一步是合并全部JS文件而后在网站中使用。最难的部分是编写Brocfile文件,这一次咱们要安装4个插件:
npm install --save-dev broccoli-babel-transpiler
npm install --save-dev broccoli-funnel
npm install --save-dev broccoli-concat
npm install --save-dev broccoli-merge-trees
把它们投入使用:
// Babel转译器
var babel = require('broccoli-babel-transpiler');
// 过滤树(文件的子集)
var funnel = require('broccoli-funnel');
// 连结树
var concat = require('broccoli-concat');
// 合并树
var mergeTrees = require('broccoli-merge-trees');
// 转译源文件
var appJs = babel('src');
// 获取Babel库提供的polyfill文件
var babelPath = require.resolve('broccoli-babel-transpiler');
babelPath = babelPath.replace(/\/index.js$/, '');
babelPath += '/node_modules/babel-core';
var browserPolyfill = funnel(babelPath, {
files: ['browser-polyfill.js']
});
// 给转译后的文件树添加Babel polyfill
appJs = mergeTrees([browserPolyfill, appJs]);
// 将全部JS文件连结为一个单独文件
appJs = concat(appJs, {
// 咱们指定一个连结顺序
inputFiles: ['browser-polyfill.js', '**/*.js'],
outputFile: '/js/my-app.js'
});
// 获取入口文件
var index = funnel('src', {files: ['index.html']});
// 获取全部的树
// 并导出最终单一的树
module.exports = mergeTrees([index, appJs]);
如今开始构建并执行咱们的代码。
broccoli build dist
此次你在dist文件夹中应该看到如下结构:
$> tree dist/
dist/
├── index.html
└── js
└── my-app.js
那是一个静态网站,你能够用任意服务器伺服来验证那段代码正常运行。举个例子:
cd dist/
python -m SimpleHTTPServer
# 访问http://localhost:8000/
你应该能够看到:
上述第二个示例给出了一个经过Babel实现功能的思路,它可能足够你用上一阵子了。若是你想要更多有关ES六、Babel和Broccoli的内容,能够查看broccoli-babel-boilerplate,这个仓库中的代码能够提供Broccoli+Babel项目的配置,并且高出至少两个层次。这个样板能够文件处理模块、模块导入以及单元测试。
经过这些配置,你能够在示例es6-modules中亲自实践。Brocfile魔力无穷,与咱们以前实现的很是相似。
正如你看到的,Babel和Broccoli对于在Web网站中应用ES6新特性很是实用。
请看这样一段代码:
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
代码乍一看有些复杂,使用了一些陌生的特性,稍后我会详细讲解每一部分。如今,一块儿来看一下咱们建立的对象:
> obj.count = 1;
setting count!
> ++obj.count;
getting count!
setting count!
2
显示结果可能与咱们的理解不太同样,为何会输出“setting count
”和“getting count
”?其实,咱们拦截了这个对象的属性访问方法,而后将“.”运算符重载了。
计算领域最好的的技巧是虚拟化,这种技术通常用来实现惊人的功能。它的工做机制以下:
随便选一张照片。
在图片中围绕某物勾勒出一个轮廓。
如今替换掉轮廓中的内容,或者替换掉轮廓外的内容,可是始终要遵循向后兼容的规则,替换先后的图片要尽量类似,不能让轮廓两侧的图像过于突兀。
你可能在《楚门的世界》和《黑客帝国》这类经典的计算机科学电影中见到过相似的hack方法,将世界划分为两个部分,主人公生活在内部世界,外部世界被精心编造的常态幻觉所替换。
为了知足向后兼容的规则,你须要巧妙地设计填补进去的图片,可是真正的技巧是正确地勾勒轮廓。
我所谓的轮廓是指一个API边界或接口,接口能够详细说明两段代码的交互方式以及交互双方对另外一半的需求。因此若是一旦在系统中设计好了接口,轮廓天然就清晰了,这样就能够任意替换接口两侧的内容而不影响两者的交互过程。
若是没有现成的接口,就须要施展你的创意才华来创造新接口,有史以来最酷的软件hack老是会勾勒一些以前从未有过的API边界,而后经过大量的工程化实践将接口引入到现有的体系中去。
虚拟内存、硬件虚拟化、Docker、Valgrind、rr等不一样抽象程度的项目都会基于现有的系统推进开发一些使人意想不到的新接口。在某些状况下,须要花费数年的时间、新的操做系统特性甚至是新的硬件来使新的边界良好运转。
最棒的虚拟化hack会带来对须要虚拟的东西的新的理解。想要编写一个API,你须要充分理解你所面向的对象,一旦你理解透彻,就能实现出使人惊异的成果。
而ES6则为JavaScript中最基本的概念“对象(object)”引入了虚拟化支持。
噢,我是说真的,请花费一点时间仔细想一想这个问题的答案。当你清楚本身知道对象是什么的的时候再向下滚动。
这个问题于我而言太难了!我从未听到过一个很是满意的定义。
这会让你感到惊讶么?定义基础概念向来很困难——抽空看看欧几里得在《几何本来》中的前几个定义你就知道了。ECMAScript语言规范很棒,但是却将对象定义为“type对象的成员”,这种定义真的对咱们没什么帮助。
后来,规范中又添加了一个定义:“对象是属性的集合”。这句话没错,目前来讲能够这样定义,咱们稍后继续讨论。
我以前说过,想要编写一个API,你须要充分理解你所面向的对象。因此在某种程度上,我也算对本文作出一个承诺,咱们会一块儿深刻理解对象的细节,而后一块儿实现酷炫的功能。
那么咱们就跟随ECMAScript标准委员会的脚步,为JavaScript对象定义一个API,一个接口。问题是咱们须要什么方法?对象又能够作什么呢?
这个问题的答案必定程度上取决于对象的类型:DOM元素对象能够作一部分事情,音频节点对象又能够作另一部分事情,可是全部对象都会共享一些基础功能:
几乎全部处理对象的JS程序都是使用属性、原型和函数来完成的。甚至元素或声音节点对象的特殊行为也是经过调用继承自函数属性的方法来进行访问。
因此ECMAScript标准委员会定义了一个由14种内部方法组成的集合,亦即一个适用于全部对象的通用接口,属性、原型和函数这三种基础功能天然成为它们关注的核心。
咱们能够在ES6标准列表5和6中找到所有的14种方法,我只会在这里讲解其中一部分。双方括号[[ ]]表明内部方法,在通常的JS代码中不可见,你能够调用、删除或覆写普通方法,可是没法操做内部方法。
obj.[[Get]](key, receiver) – 获取属性值。
当JS代码执行如下方法时被调用:obj.prop
或obj[key]
。
obj是当前被搜索的对象,receiver是咱们首先开始搜索这个属性的对象。有时咱们必需要搜索几个对象,obj多是一个在receiver原型链上的对象。
obj.[[Set]](key, value, receiver) – 为对象的属性赋值。
当JS代码执行如下方法时被调用:obj.prop = value
或obj[key] = value
。
执行相似obj.prop += 2
这样的赋值语句时,首先调用[[Get]]方法,而后调用[[Set]]方法。对于++和--操做符来讲亦是如此。
obj.[HasProperty] – 检测对象中是否存在某属性。
当JS代码执行如下方法时被调用:key in obj
。
obj.[Enumerate] – 列举对象的可枚举属性。
当JS代码执行如下方法时被调用:for (key in obj)
…
这个内部方法会返回一个可迭代对象,for-in
循环可经过这个方法获得对象属性的名称。
obj.[GetPrototypeOf] – 返回对象的原型。
当JS代码执行如下方法时被调用:obj.[__proto__]
或Object.getPrototypeOf
(obj)
。
functionObj.[[Call]](thisValue, arguments) – 调用一个函数。
当JS代码执行如下方法时被调用:functionObj()
或x.method()
。
可选的。不是每个对象都是函数。
constructorObj.[[Construct]](arguments, newTarget) – 调用一个构造函数。
当JS代码执行如下方法时被调用:举个例子,new Date(2890, 6, 2)
。
可选的。不是每个对象都是构造函数。
参数newTarget在子类中起必定做用,咱们将在将来的文章中详细讲解。
可能你也能够猜到其它七个内部方法。
在整个ES6标准中,只要有可能,任何语法或对象相关的内建函数都是基于这14种内部方法构建的。ES6在对象的中枢系统周围划分了一个清晰的界限,你能够借助代理特性用任意JS代码替换标准中枢系统的内部方法。
既然咱们立刻要开始讨论覆写内部方法的相关问题,请记住,咱们要讨论的是诸如obj.prop
的核心语法、诸如Object.keys()
的内建函数等的行为。
ES6规范定义了一个全新的全局构造函数:代理(Proxy)。它能够接受两个参数:目标对象(target)与句柄对象(handler)。请看一个简单的示例:
var target = {}, handler = {};
var proxy = new Proxy(target, handler);
咱们先来探讨代理和目标对象之间的关系,而后再研究句柄对象的功用。
代理的行为很简单:将代理的全部内部方法转发至目标。简单来讲,若是调用proxy.[[Enumerate]]()
,就会返回target.[[Enumerate]]()
。
如今,让咱们尝试执行一条可以触发调用proxy.[[Set]]()
方法的语句。
proxy.color = "pink";
好的,刚刚都发生了什么?proxy.[[Set]]()
应该调用target.[[Set]]()
方法,而后在目标上建立一个新的属性。实际的结果如何?
> target.color
"pink"
是的,它作到了!对于全部其它内部方法而言一样能够作到。新建立的代理会尽量与目标的行为一致。
固然,它们也不彻底相同,你会发现proxy !== target
。有时也有目标可以经过类型检测而代理没法经过的状况发生,举个例子,若是代理的目标是一个DOM元素,相应的代理就不是,此时相似document.body.appendChild(proxy)
的操做会触发类型错误(TypeError
)。
如今咱们继续来讨论一个让代理充满魔力的功能:句柄对象。
句柄对象的方法能够覆写任意代理的内部方法。
举个例子,你能够定义一个handler.set()
方法来拦截全部给对象属性赋值的行为:
var target = {};
var handler = {
set: function (target, key, value, receiver) {
throw new Error("请不要为这个对象设置属性。");
}
};
var proxy = new Proxy(target, handler);
> proxy.name = "angelina";
Error: 请不要为这个对象设置属性。
句柄方法的完整列表能够在MDN有关代理的页面上找到,一共有14种方法,与ES6中定义的14中内部方法一致。
全部句柄方法都是可选的,没被句柄拦截的内部方法会直接指向目标,与咱们以前看到的别无二致。
到目前为止,咱们对于代理的了解程度足够尝试去作一些奇怪的事情,实现一些不借助代理根本没法实现的功能。
咱们的第一个实践,建立一个Tree()
函数来实现如下特性:
> var tree = Tree();
> tree
{ }
> tree.branch1.branch2.twig = "green";
> tree
{ branch1: { branch2: { twig: "green" } } }
> tree.branch1.branch3.twig = "yellow";
{ branch1: { branch2: { twig: "green" },
branch3: { twig: "yellow" }}}
请注意,当咱们须要时,全部中间对象branch1、branch2和branch3均可以自动建立。这当然很方便,可是如何实现呢?
在这以前,没有能够实现这种特性的方法,可是经过代理,咱们只用寥寥几行就能够轻松实现,而后只须要接入tree.[[Get]]()
就能够。若是你喜欢挑战,在继续阅读前能够尝试本身实现。
这里是个人解决方案:
function Tree() {
return new Proxy({}, handler);
}
var handler = {
get: function (target, key, receiver) {
if (!(key in target)) {
target[key] = Tree(); // 自动建立一个子树
}
return Reflect.get(target, key, receiver);
}
};
注意最后的Reflect.get()
调用,在代理句柄方法中有一个极其常见的需求:只执行委托给目标的默认行为。因此ES6定义了一个新的反射(Reflect)对象
,在其上有14种方法,你能够用它来实现这一需求。
我想我可能传达给大家一个错误的印象,也就是代理易于使用。接下来的这个示例可能会让你稍感困顿。
这一次咱们的赋值语句更复杂:咱们须要实现一个函数,readOnlyView(object)
,它能够接受任何对象做为参数,并返回一个与此对象行为一致的代理,该代理不可被变动,就像这样:
> var newMath = readOnlyView(Math);
> newMath.min(54, 40);
40
> newMath.max = Math.min;
Error: can't modify read-only view
> delete newMath.sin;
Error: can't modify read-only view
即便咱们不会阻断内部方法的行为,但仍然要对其进行干预,因此第一步是拦截可能修改目标对象的五种内部方法。
function NOPE() {
throw new Error("can't modify read-only view");
}
var handler = {
// 覆写全部五种可变方法。
set: NOPE,
defineProperty: NOPE,
deleteProperty: NOPE,
preventExtensions: NOPE,
setPrototypeOf: NOPE
};
function readOnlyView(target) {
return new Proxy(target, handler);
}
这段代码能够正常运行,它借助只读视图阻止了赋值、属性定义等过程。
最大的问题是相似[[Get]]的一些方法可能仍然返回可变对象,因此即便一些对象x
是只读视图,x.prop
多是可变的!这是一个巨大的漏洞。
咱们须要添加一个handler.get()
方法来堵上漏洞:
var handler = {
...
// 在只读视图中包裹其它结果。
get: function (target, key, receiver) {
// 从执行默认行为开始。
var result = Reflect.get(target, key, receiver);
// 确保返回一个不可变对象!
if (Object(result) === result) {
// result是一个对象。
return readOnlyView(result);
}
// result是一个原始原始类型,因此已经具有不可变的性质。
return result;
},
...
};
这仍然不够,getPrototypeOf
和getOwnPropertyDescriptor
这两个方法也须要进行一样的处理。
然而还有更多问题,当经过这种代理调用getter或方法时,传递给getter或方法的this
的值一般是代理自身。可是正如咱们以前所见,有时代理没法经过访问器和方法执行的类型检查。在这里用目标对象代替代理更好一些。聪明的小伙伴,你知道如何解决这个问题么?
因而可知,建立代理很是简单,可是建立一个具备直观行为的代理至关困难。
代理到底好在哪里?
代理能够帮助你观察或记录对象访问,当调试代码时助你一臂之力,测试框架也能够用代理来建立模拟对象(mock object)。
代理能够帮助你强化普通对象的能力,例如:惰性属性填充。
我不太想提到这一点,可是若是要想了解代理在代码中的运行方式,将代理的句柄对象包裹在另外一个代理中是一个很是不错的办法,每当句柄方法被访问时就能够将你想要的信息输出到控制台中。
正如上文中只读视图的示例readOnlyView
,咱们能够用代理来限制对象的访问。固然在应用代码中不多遇到这种用例,可是Firefox在内部使用代理来实现不一样域名之间的安全边界,是咱们的安全模型的关键组成部分。
与WeakMap深度结合。在咱们的readOnlyView
示例中,每当对象被访问的时候建立一个新的代理。这种作法能够帮助咱们节省在WeakMap
中建立代理时的缓存内存,因此不管传递多少次对象给readOnlyView
,只会建立一个代理。
这也是一个动人的WeakMap用例。
代理可解除。ES6规范中还定义了另一个函数:Proxy.revocable(target, handler)
。这个函数能够像new Proxy(target, handler)
同样建立代理,可是建立好的代理后续可被解除。(Proxy.revocable
方法返回一个对象,该对象有一个.proxy
属性和一个.revoke
方法。)一旦代理被解除,它即刻中止运行并抛出全部内部方法。
对象不变性。在某些状况下,ES6须要代理的句柄方法来报告与目标对象状态一致的结果,以此来保证全部对象甚至是代理的不变性。举个例子,除非目标不可扩展(inextensible),不然代理不能被声明为不可扩展的。
不变性的规则很是复杂,在此不展开详述,可是若是你看到相似“proxy can't report a non-existent property as non-configurable
”这样的错误信息,就能够考虑从不变性的角度解决问题,最可能的补救方法是改变代理报告自己,或者在运行时改变目标对象来反射代理的报告指向。
我记得咱们以前的看法是:“对象是属性的集合。”
我不喜欢这个定义,即便给定义叠加原型和可调用能力也不会让我改变见解。我认为“集合(collection)”这个词太危险了,不适合用做对象的定义。对象的句柄方法能够作任何事情,它们也能够返回随机结果。
ECMAScript标准委员会针对这个问题开展了许多研究,搞清楚了对象能作的事情,将那些方法进行标准化,并将虚拟化技术做为每一个人都能使用的一等特性添加到语言的新标准中,为前端开发领域拓展了无限可能。
完善后的对象几乎能够表示任何事物。
对象是什么?可能如今最贴切的答案须要用12个内部方法进行定义:对象是在JS程序中拥有[[Get]]、[[Set]]等操做的实体。
我不太肯定咱们是否比以前更了解对象,可是咱们绝对作了许多惊艳的事情,是的,咱们实现了旧版JS根本作不到的功能。
不!在Web平台上不管如何都不行。目前只有Firefox和微软的Edge支持代理,并且尚未支持这一特性polyfill。
若是你想在Node.js或io.js环境中使用代理,首先你须要添加名为harmony-reflect的polyfill,而后在执行时启用一个非默认的选项(--harmony_proxies
),这样就能够暂时使用V8中实现的老版本代理规范。
放轻松,让咱们一块儿来作试验吧!为每个对象建立成千上万个类似的副本镜像却不能调试?如今就解放本身!不过目前来看,请不要将欠考虑的有关代理的代码泄露到产品中,这很是危险。
代理特性在2010年由Andreas Gal首先实现,由Blake Kaplan进行代码审查。标准委员会后来彻底从新设计了这个特性。Eddy Bruel在2012年实现了新标准。
我实现了反射(Reflect)
特性,由Jeff Walden进行代码审查。Firefox Nightly已经支持除Reflect.enumerate()
外的全部特性。