原文:http://addyosmani.com/blog/a-few-new-things-coming-to-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中可以體驗新的模塊語法,你會非常容易的熟悉這些語法,該語法可能會覺得有點像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都有自己的解決方案.
這是一個非常重要的新功能,它不僅比目前所有框架的同類實現性能要好,而且還能更容易的觀察純原生對象.
- // 一個簡單的對象可以作為一個模塊來使用
- 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+等等,都有這個問題.
塊級作用域
塊級作用域引入了兩種新的聲明形式,可以用它們定義一個只存在於某個語句塊中的變量或常量.這兩種新的聲明關鍵字為:
let
: 語法上非常類似於var
, 但定義的變量只存在於當前的語句塊中const
: 和let
類似,但聲明的是一個只讀的常量
使用let
代替var
可以更容易的定義一個只在某個語句塊中存在的局部變量,而不用擔心它和函數體中其他部分的同名變量有沖突.在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?id=harmony:proposals
總結
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中新知識的一小部分,而且明顯作者自己也有點趕不上草案的快速變化(本文中提到的所有知識點都有可能在明天就發生變化).所以本文的內容僅僅是個開始,原文中的外部連接加上我給出的外部鏈接才是最需要你關注的.