ES6:JavaScript 新特性
我相信,在ECMAScript.next到来的时候,我们如今每天都在写的JavaScript代码将会发生巨大的变化.接下来的一年将会是令JavaScript开发人员们兴奋的一年,越来越多的特性提案将被终于敲定,新一版本号的JavaScript将会慢慢得到普及.
本文中,我将会讲几个我个人非常期待的,希望能在2013年或者更晚一点使用上的新特性.
ES.next眼下的实现情况
能够通过查看Juriy Zaytsev总结的ECMAScript 6兼容性表格,和Mozilla的ES6实现情况页面以及通过使用现代浏览器的最新版本号(比方Chrome Canary, Firefox Aurora),来了解眼下有哪些已经实现了的ES.next特性.
在Canary中,记得要进入chrome:flags
打开'启用实验性JavaScript'选项以激活全部最新的的JavaScript特性.
另外,很多ES.next特性还能够通过使用Google的Traceur转换编译器(这里有一些单元測试的样例)来体验,以及一些shim项目比方ES6-Shim和Harmony Collections,也实现了不少新特性.
在Node.js(V8)中使用--harmony
命令行选项能够开启一些试验性质的ES.next特性,包含块级作用域,WeakMap等等.
模块
我们已经习惯了将我们的代码切割成为更加便于管理的功能块.在ES.next中,一个模块(module)是就是一个module
声明,以及包括在该声明中的一组代码.模块能够用内联方式(inline)声明,也能够引入一个外部的模块文件.一个名为Car的内联模块的写法大致例如以下:
- module Car {
- // 导入 …
- // 导出 …
- }
一个模块实例就是一个被求过值的模块,它已经被链接到了其它的模块身上或者已经有了词法上的封装数据.以下是一个模块实例的样例:
- module myCar at "car.js";
module
声明能够使用在例如以下上下文中:
- module UniverseTest {};
- module Universe { module MilkyWay {} };
- module MilkyWay = 'Universe/MilkyWay';
- module SolarSystem = Universe.MilkyWay.SolarSystem;
- module MySystem = SolarSystem;
一个export
声明声明了一个能够被其它模块看到的局部函数或变量.
- module Car {
- // 内部变量
- var licensePlateNo = '556-343';
- // 暴露到外部的变量和函数
- export function drive(speed, direction) {
- console.log('details:', speed, direction);
- }
- export module engine{
- export function check() { }
- }
- export var miles = 5000;
- export var color = 'silver';
- };
一个模块能够使用import
导入不论什么它所须要的其它模块.导入模块会读取被导入模块的全部可导出数据(比方上面的drive()
, miles
等),但不能改动它们.导出的变量或函数能够被重命名.
再次用到上面导出相关的样例,我们如今能够有选择性的导入一些模块中的功能.
比方我们能够导入drive()
:
- import drive from Car;
还能够能够同一时候导入drive()
和miles
:
- import {drive, miles} from Car;
以下,我们要讲一下模块载入器API的概念.模块载入器可以让我们动态的载入所须要的脚本.类似于import
, 我们可以使用被导入模块中的全部用export
声明过的东西.
- // Signature: load(moduleURL, callback, errorCallback)
- Loader.load('car.js', function(car) {
- console.log(car.drive(500, 'north'));
- }, function(err) {
- console.log('Error:' + err);
- });
load()
接受三个參数:
moduleURL
: 表示一个模块URL的字符串 (比方 "car.js")callback
: 一个回调函数,接受模块载入,编译,以及运行后的输出结果errorCallback
: 一个回调函数,在载入或编译期间错误发生时调用
关于类(class)
我不打算在本文中过多的讲ES.next中的类,假设你想知道类和模块将会有什么联系,Alex Russell以前写过一个非常好的样例来说明这件事.
JavaScript中有了类,并不意味着要把JavaScript变成Java.ES.next中的类仅仅是我们已经熟悉的语义(比方函数,原型)的第二种声明方式
以下是用来定义一个widget的ES.next代码:
- module widgets {
- // ...
- class DropDownButton extends Widget {
- constructor(attributes) {
- super(attributes);
- this.buildUI();
- }
- buildUI() {
- this.domNode.onclick = function(){
- // ...
- };
- }
- }
- }
以下是去糖(de-sugared)后的做法,也就是我们眼下正在使用的方法:
- var widgets = (function(global) {
- // ...
- function DropDownButton(attributes) {
- Widget.call(this, attributes);
- this.buildUI();
- }
- DropDownButton.prototype = Object.create(Widget.prototype, {
- constructor: { value: DropDownButton },
- buildUI: {
- value: function(e) {
- this.domNode.onclick = function(e) {
- // ...
- }
- }
- }
- });
- })(this);
ES.next的写法的确让代码变的更可读.这里的class
也就相当于是function
,至少是做了眼下我们用function
来做的一件事.假设你已经习惯而且也喜欢用JavaScript中的函数和原型,这样的未来的语法糖也就不用在意了.
这些模块怎样和AMD配合使用?
ES.next中的模块是朝着正确的方向走了一步吗?或许是吧.我自己的看法是:看相关的规范文档是一码事,实际上使用起来又是还有一码事.在Harmonizr,Require
HM和Traceur中能够体验新的模块语法,你会很easy的熟悉这些语法,该语法可能会认为有点像Python的感觉(比方import
语句).
我觉得,假设一些功能有足够广泛的使用需求(比方模块),那么平台(也就是浏览器)就应该原生支持它们.并且,并非仅仅有我一个人这么觉得. James Burke,发明了AMD和RequireJS的人,也以前说过:
我想,AMD和RequireJS应该被淘汰了.它们的确攻克了一个实际存在的问题,但更理想的情况是,语言和执行环境应该内置类似的功能.模块的原生支持应该可以覆盖RequireJS 80%的使用需求,从这一点上说,我们不再须要使用不论什么用户态(userland)的模块载入库了,至少在浏览器中是这样.
只是James的质疑是ES.next的模块是否是一个足够好的解决方式,他曾在六月份谈到过自己关于ES.next中模块的一些想法 ES6 Modules: Suggestions for improvement以及再后来的一篇文章Why not AMD?.
Isaac Schlueter前段时间也写过一些自己的想法,讲到了ES6的模块有哪些不足.尝试一下以下这些选项,看看你的想法怎样.
兼容眼下引擎的Module实现
Object.observe()
通过Object.observe
,我们能够观察指定的对象,而且在该对象被改动时得到通知.这样的改动操作包含属性的加入,更新,删除以及又一次配置.
属性观察是我们常常会在MVC框架中看到的行为,它是数据绑定的一个重要组件,AngularJS和Ember.js都有自己的解决方式.
这是一个很重要的新功能,它不仅比眼下全部框架的同类实现性能要好,并且还能更easy的观察纯原生对象.
-
// 一个简单的对象能够作为一个模块来使用
- var todoModel = {
- label: 'Default',
- completed: false
- };
-
// 我们观察这个对象
- Object.observe(todoModel, function(changes) {
- changes.forEach(function(change, i) {
- console.log(change);
- /*
- 哪个属性被改变了? change.name
-
改变类型是什么?
change.type
-
新的属性值是什么?
change.object[change.name]
- */
- });
- });
- // 使用时:
- todoModel.label = 'Buy some more milk';
- /*
-
label属性被改变了
-
改变类型是属性值更新
- 当前属性值为'Buy some more milk'
- */
- todoModel.completeBy = '01/01/2013';
- /*
-
completeBy属性被改变了
-
改变类型是属性被加入
- 当前属性值为'01/01/2013'
- */
- delete todoModel.completed;
- /*
-
completed属性被改变了
-
改变类型是属性被删除
- 当前属性值为undefined
- */
Object.observe立即将会在Chrome Canary中实现(须要开启"启用实验性JavaScript"选项).
兼容眼下引擎的Object.observe()实现
- Chromium特殊版本号
- Watch.JS 能够实现类似的功能,但它并非实现
Object.observe
的polyfill或shim
Rick Waldron的这篇文章有关于Object.observe
更具体的介绍.
译者注:Firefox非常早就有了一个类似的东西:Object.prototype.watch.
默认參数值
默认參数值(Default
parameter values)的作用是:在一些形參没有被显式传值的情况下,使用默认的初始化值来进行初始化.这就意味着我们不再须要写类似options = options || {};
这种语句了.
该语法形式就是把一个初始值赋值给相应的形參名:
- function addTodo(caption = 'Do something') {
- console.log(caption);
- }
- addTodo(); // Do something
拥有默认參数值的形參仅仅能放在形參列表的最右边:
- function addTodo(caption, order = 4) {}
- function addTodo(caption = 'Do something', order = 4) {}
- function addTodo(caption, order = 10, other = this) {}
已经实现该特性的浏览器: Firefox 18+
译者注:Firefox 15就已经实现了默认參数值,作者所说的18仅仅是说18支持,并非说18是第一个支持的版本号.包含本文以下将要提到的chrome 24+等等,都有这个问题.
块级作用域
块级作用域引入了两种新的声明形式,能够用它们定义一个仅仅存在于某个语句块中的变量或常量.这两种新的声明keyword为:
let
: 语法上很类似于var
, 但定义的变量仅仅存在于当前的语句块中const
: 和let
类似,但声明的是一个仅仅读的常量
使用let
取代var
能够更easy的定义一个仅仅在某个语句块中存在的局部变量,而不用操心它和函数体中其它部分的同名变量有冲突.在let
语句内部用var
声明的变量和在let
语句外部用var
声明的变量没什么区别,它们都拥有函数作用域,而不是块级作用域.
译者注:以防读者看不懂,我用一个样例解释一下上面的这句话,是这种:
let(var1 = 1) {
alert(var1); //弹出1,var1是个块级作用域变量 var var2 = 2;
}
var var3 = 3;
alert(var2); //弹出2,尽管var2是在let语句内部声明的,但它仍然是个函数作用域内的变量,由于使用的是var声明 alert(var3); //弹出3 alert(var1); //抛出异常
- var x = 8;
- var y = 0;
- let (x = x+10, y = 12) {
- console.log(x+y); // 30
- }
- console.log(x + y); // 8
实现let
的浏览器: Firefox 18+, Chrome 24+
实现const
的浏览器: Firefox 18+, Chrome 24+, Safari 6+, WebKit, Opera 12+
译者注:Firefox非常久曾经就支持了let和const,但这两个旧的实现都是根据了当年的ES4草案.和眼下的ES6草案有些区别,比方ES4中用const声明的常量并没有块级作用域(和var一样,仅仅是值不可变),let也有一些细微区别,就不说了.因为非常少人使用旧版的Firefox(但我的主浏览器是FF3.6!),即使未来ES6和ES4中的一些东西有冲突,我们基本也能够忽略.
Map
我想大部分读者已经熟悉了映射的概念,由于我们过去一直都是用纯对象来实现映射的.Map同意我们将一个值映射到一个唯一的键上,然后我们就能够通过这个键获取到相应的值,而不须要操心用普通对象实现映射时因原型继承而带来的问题.
使用set()
方法,能够在map中加入一个新的键值对,使用get()
方法,能够获取到所存储的值.Map对象还有其它三个方法:
has(key)
: 一个布尔值,表明某个键是否存在于map中delete(key)
: 删除掉map中指定的键size()
: 返回map中键值对的个数
- let m = new Map();
- m.set('todo', 'todo'.length); // "todo" → 4
- m.get('todo'); // 4
- m.has('todo'); // true
- m.delete('todo'); // true
- m.has('todo'); // false
已经实现Map的浏览器: Firefox 18+
Nicholas Zakas的这篇文章有关于Map更具体的介绍.
兼容眼下引擎的Map实现
译者注:我翻译过尼古拉斯的这篇文章:[译]ECMAScript 6中的集合类型,第二部分:Map.
作者可能不知道,10月份的ES6草案中,
Map.prototype.size
和Set.prototype.size
都从size()方法改成size訪问器属性了.同一时候Map对象新加入的方法还有非常多,clear()用来清空一个map,forEach()用来遍历一个map,还有items(),keys(),values()等.Set对象也类似,有不少作者没提到的方法,以下的Set小节我就不指出了.另外,在ES5中,在把对象当成映射来使用的时候,为了防止原型继承带来的问题(比方在twitter中,@__proto__能让浏览器卡死),能够用var hash = Object.create(null)取代var hash = {};
Set
正如Nicholas Zakas在他的文章中所说,对于那些接触过Ruby和Python等其它语言的程序猿来说,Set并非什么新东西,但它的确是在JavaScript中一直都缺少的特性.
不论什么类型的数据都能够存储在一个set中,但每一个值仅仅能存储一次(不能反复).利用Set能够非常方便的创建一个不包括不论什么反复值的有序列表.
add(value)
– 向set中加入一个值.delete(value)
– 从set中删除value这个值.has(value)
– 返回一个布尔值,表明value这个值是否存在于这个set中.
- let s = new Set([1, 2, 3]); // s有1, 2, 3三个元素.
- s.has(-Infinity); // false
- s.add(-Infinity); // s有1, 2, 3, -Infinity四个元素.
- s.has(-Infinity); // true
- s.delete(-Infinity); // true
- s.has(-Infinity); // false
Set对象的一个作用是用来减少过滤操作(filter方法)的复杂度.比方:
- function unique(array) {
- var seen = new Set;
- return array.filter(function (item) {
- if (!seen.has(item)) {
- seen.add(item);
- return true;
- }
- });
- }
这个利用Set来进行数组去重的函数的复杂度为O(n).而其它现有数组去重方法的复杂度差点儿都为O(n^2).
已经实现Set的浏览器: Firefox 18, Chrome 24+.
Nicholas Zakas的这篇文章有关于Set更具体的介绍.
兼容眼下引擎的Set实现
译者注:我翻译过尼古拉斯的这篇文章:[译]ECMAScript 6中的集合类型,第一部分:Set.
假设让我来实现一个ES6下的数组去重函数的话,我会这么写:
function unique(array) {
return [v for(v of Set(array))]
}该函数使用到了ES6中的for-of遍历,以及数组推导式.只是效率比上面使用filter去重的方法略微差点.Firefox最新版中已经能够运行这个函数.
另外,借助于以下将会提到的Array.from方法,还有更简单高效的写法:
>Array.from(new Set([ 1, 1, 2, 2, 3, 4 ]));
[1,2,3,4]甚至,借助于ES6中的展开(spread)操作,还有可能这样实现:
>[ ... new Set([ 1, 1, 2, 2, 3, 4 ]) ];
[1,2,3,4]
WeakMap
WeakMap的键仅仅能是个对象值,并且该键持有了所引用对象的弱引用,以防止内存泄漏的问题.这就意味着,假设一个对象除了WeakMap的键以外没有不论什么其它的引用存在的话,垃圾回收器就会销毁这个对象.
WeakMap的另外一个特点是我们不能遍历它的键,而Map能够.
- let m = new WeakMap();
- m.set('todo', 'todo'.length); // 异常,键必须是个对象值!
- // TypeError: Invalid value used as weak map key
- m.has('todo'); // 相同异常!
- // TypeError: Invalid value used as weak map key
- let wmk = {};
- m.set(wmk, 'thinger'); // wmk → 'thinger'
- m.get(wmk); // 'thinger'
- m.has(wmk); // true
- m.delete(wmk); // true
- m.has(wmk); // false
已经实现WeakMap的浏览器: Firefox 18+, Chrome 24+.
兼容眼下引擎的WeakMap实现
Nicholas Zakas的这篇文章有关于WeakMap更具体的介绍.
译者注:我翻译过尼古拉斯的这篇文章:[译]ECMAScript 6中的集合类型,第三部分:WeakMap
代理
代理(Proxy)API同意你创建一个属性值在执行期间动态计算的对象.还能够利用代理API"钩入"其它的对象,实现比如打印记录和赋值审核的功能.
- var obj = {foo: "bar"};
- var proxyObj = Proxy.create({
- get: function(obj, propertyName) {
- return 'Hey, '+ propertyName;
- }
- });
- console.log(proxyObj.Alex); // "Hey, Alex"
实现代理API的浏览器: Firefox 18+, Chrome 24+
译者注:作者不知道的是,一共同拥有过两个代理API的提案,一个是旧的Catch-all Proxies,一个是新的直接代理(Direct Proxies).前者已被废弃.两者的差别在这里.V8(Chrome和Node.js)实现的是前者,Firefox18及之后版本号实现的是后者(17及之前版本号实现的是前者).尼古拉斯在2011年写的文章也应该是过时的.
一些新的API
Object.is
Object.is
是一个用来比較两个值是否相等的函数.该函数和===
最基本的差别是在对待特殊值NaN
与自身以及正零与负零之间的比較上.Object.is
的推断结果是:NaN
与另外一个NaN
是相等的,以及+0和-0是不等的.
- Object.is(0, -0); // false
- Object.is(NaN, NaN); // true
- 0 === -0; // true
- NaN === NaN; // false
实现了Object.is的浏览器: Chrome 24+
兼容眼下引擎的Object.is实现
译者注:Object.is方法和严格相等===运算符的差别体如今ES标准内部就是SameValue算法和严格相等比較算法的差别.
译者注:假设我没有理解错这条BE的推特的话,Object.is要改名成为Object.sameValue了.
Array.from
Array.from
: 将參数中的类数组(array-like)对象(比方arguments, NodeList, DOMTokenList (classList属性就是这个类型), NamedNodeMap (attributes属性就是这个类型))转换成数组并返回,比方转换一个纯对象:
- Array.from({
- 0: 'Buy some milk',
- 1: 'Go running',
- 2: 'Pick up birthday gifts',
- length: 3
- });
再比方转换一个DOM节点集合:
- var divs = document.querySelectorAll('div');
- Array.from(divs);
- // [<div class="some classes" data-info="12"></div>, <div data-info="10"></div>]
- Array.from(divs).forEach(function(node) {
- console.log(node);
- });
兼容眼下引擎的Array.from实现
译者注:从作者举的两个样例能够看出,Array.from基本相当于眼下使用的[].prototype.slice.call.
眼下的草案也的确是这样规定的,但从Rick Waldron(TC39成员)在原文评论中给出的代码能够看出,或许Array.from未来也能将Set对象(非类数组对象,但可迭代)转换成数组.
译者注:除了这两个API,还有非常多个新加入的API,比方
Number.isFinite, isNaN, isInteger, toInteger
String.prototype.repeat, startsWith, endsWith, contains, toArray
以下给出两个非常实用的链接:
Mozilla正计划实现的ES6特性https://wiki.mozilla.org/ES6_plans.
ES6眼下的全部特性提案http://wiki.ecmascript.org/doku.php?
总结
ES.next中加入了很多被觉得是JavaScript中缺失已久的新特性.尽管ES6规范计划在2013年年底公布,只是浏览器们已经開始实现当中的一些特性了,这些特性被广泛使用也仅仅是时间问题了.
在ES6全然实现之前,我们能够使用一些转换编译器(transpiler)或者shim来体验当中一些特性.
译者注:眼下最强大的ES6实现应该是Brandon Benvie写的continuum,这是一个JavaScript虚拟机,也就是用JavaScript(ES3)实现的JavaScript(ES6)引擎,它未来甚至能够工作在IE6上.眼下实现的ES6特性有:模块以及模块载入器API,直接代理,生成器,解构,@symbols(我翻译成标志,这是一个不可能通过shim方式实现的语法)等等.
想要查看很多其它的样例和了解最新的信息,能够去TC39 Codex Wiki,该网站由Dave Herman和其它一些EC39成员维护(译者注:该新站仍在建设中,应该訪问旧站).当中包括了下一代JavaScript中将要有的全部新特性.
激动人心的时刻立即就要到来了!
译者注:文本中提到的知识点不过ES6中新知识的一小部分,并且明显作者自己也有点赶不上草案的高速变化(本文中提到的全部知识点都有可能在明天就发生变化).所以本文的内容不过个開始,原文中的外部连接加上我给出的外部链接才是最须要你关注的.