前言

有慣常留意 Node.js 社群的開發者們應該都知道剛剛有一個重大更新,現時最新的版本到了 v4.1。然而根據 Node.js 在發佈 v4.0 的時候釋放的官方文檔,指出剛在六月正式發佈 ECMAScript 6 (下稱 ES6) 會分三個階段納入最新的版本當中。它們分別是:

  • Shipping features: 已經完成整合並且被 V8 開發團隊視為穩定
  • Staged features: 大致完成整合但並不能確定能夠穩定運行
  • In progress features 僅用於測試

(註: 在 Node.js v4.0 或更新版本的環境中)

除了 Shipping features 以外,開發者如要使用其他的語法特性需要自行承擔風險。

如何在 Node.js 啟用對 ES6 的支援

在 v4.0 及其以後的更新版本

  • Shipping features 並不需要加上 runtime flag 已經可以直接在最新版本的 Node.js 環境中使用
  • Staged features 需要加上 runtime flag (--es_staging--harmony) 才可以使用
  • In progress features 需要加上 runtime flag --harmony_<name>, 在 harmony_ 後面的是那語法特性的名稱,如果開發者想知道有甚麼是正在整合當中的話,可以使用node --v8-options | grep "in progress"去查詢

在 v4.0 下的 In progress 特性:

--harmony_modules (enable "harmony modules" (in progress))
--harmony_array_includes (enable "harmony Array.prototype.includes" (in progress))
--harmony_regexps (enable "harmony regular expression extensions" (in progress))
--harmony_proxies (enable "harmony proxies" (in progress))
--harmony_sloppy (enable "harmony features in sloppy mode" (in progress))
--harmony_unicode_regexps (enable "harmony unicode regexps" (in progress))
--harmony_reflect (enable "harmony Reflect API" (in progress))
--harmony_destructuring (enable "harmony destructuring" (in progress))
--harmony_sharedarraybuffer (enable "harmony sharedarraybuffer" (in progress))
--harmony_atomics (enable "harmony atomics" (in progress))
--harmony_new_target (enable "harmony new.target" (in progress))

在 v4.0 以前的版本

由於在 v4.0 以前的版本並不原生支援 ES6 的語法特性,所以我們需要一個 JavaScript 編譯器,把 ES6 的語法轉換成 ES5 的版本。Babel 是一個開源專案,如果需要在舊的 Node.js 環境中寫 ES6 的語法就可以用到它,使用的方法很容易,先用 npm 把它安裝起來。

$npm install babel -g

然後就直接可以轉換 ES6 的語法

babel myes6.js

Shipping Features

以下這些語法特性已經在最新的版本環境釋出:

索引

let, const

ES6 引入了塊級域的變量(block scope variables),使變量的作用域限制於兩個括號裡頭。跟 var 不同的是 var 所定義的變量要麼是全局(global),要麼是函數域(function scope),不能是塊級域的。比對以下例子就會明白。

var globalVar = 1;
if (true) {
  globalVar = 3;
}
console.log(globalVar); // 3

若使用 let 定義變數的話,在 Block 以外想要知道它的值是不能夠的

if (true) {
  let blockVar = 3;
}
console.log(blockVar); // undefined

錯誤使用 let 所引起的問題可以參照這裡

至於 const 固名思義就是常數,是不可變的(immutable)。

const constant = 3;
constant = 0;
console.log(constant); // 3

同一個名的常數亦不能重覆宣告,否則會引起 TypeError

const constant = 3;
const constant = 3; // TypeError: Identifier 'constant' has already been declared

class

事實上這不是一種新加入的面向編程概念,然而這只是把現有 JavaScript 裡頭基於原型(prototype)的繼承(inheritance)做法重新包裝,是一種語法糖(syntax sugar)而已,使程式碼更加簡單易明。看看在 ES6 之前的做法是如何:

var Plane = function () {}
Plane.prototype.landing = function () {}
function A380 () {}
A380.prototype = new Plane();
var emirates_a380 = new A380 ();
console.log(emirates_a380 instanceof A380); // true
console.log(emirates_a380 instanceof Plane); // true

再看看在 ES6 裡頭使用 class

'use strict';

class Plane {
  constructor () {
      // ...
  }
  takeoff () {
    console.log('Taking off');
  }
}

class A380 extends Plane {
  constructor () {
    super();
  }
}

var emirates_a380 = new A380();
console.log(emirates_a380 instanceof A380); // true
console.log(emirates_a380 instanceof Plane); // true

結果顯而易見,程式碼看起上來更直覺,更清楚易明。

Map

這個物件就是簡單的鍵/值(key/value)對應表,長久以來人們都是使用 Object 來實現 Map 的功能,事實上 ES6 所引入的 Map 還是跟 Object 有所分別:

  • 所有 Object 物件的原型都會是 Object 的預設鍵 Object.prototype。可以使用 map = Object.create(null) 去創建一個沒有原型的 Object
  • Object 的鍵只可以是字符串(String),但 Map 的鍵可以是原始數據 (Primitive) 或 Object。
  • Map 的迭代是根據 insertion order,而 Object 的迭代並沒有規範。
  • Map 新增了許多額外方法,例如計算有多少對鍵值,從前需要 Object.keys(myObj).length,現在則有 Map.prototype.size

WeakMap

WeakMap 都是簡單的鍵/值(key/value)對應表,但鍵只可以是 Object 型別,例如:

var wm1 = new WeakMap(),
    k1 = {},
    k2 = function () {},
    k3 = undefined;

wm1.set(k1,3);
wm1.get(k1); // 3
wm1.set(k2,4);
wm1.get(k2); // 4
wm1.get(k3); // undefined

Set

如果說 Map 類似 Object, 那麼也可以用 Array 去實作 Set, 但 Set 值不能重複亦不能直接提取某個位置的值,只可以知道有沒有這個值,如需要知道所有值則使用 forEach 迭代。

var s1 = new Set();
s1.add(1);
s1.add(5);
// Set { 1,5 }

但加入 Object 需要小心,可以看看以下例子

var s1 = new Set();
s1.add({c:3});
s1.add({c:3});
// Set { { c: 3 }, { c: 3 } }

這樣的話就會看似是重覆了,建議先賦值後加入,又或者使用 Map 去代替。

var s1 = new Set();
var o = { c: 3 };
s1.add(o);
s1.add(o);
// Set { { c: 3 } }
var s2 = new Set();
var m1 = new Map();
m1.set('c',3);
s2.add(m1);
// Set { Map { 'c' => 3 } }

WeakSet

WeakSet 的限制跟 WeakMap 一樣,只可以加入 Object 值而不能是原始數據 (只有 {} 以及 function () {}), 為何是 Weak, 因為 WeakSet 裡面所存儲的值都是被弱引用,所以如果沒有其他變量引用該值的話,就不能避免被回收掉 (garbage collection)。

var ws = new WeakSet();
ws.add({c:3});
ws.add(function(){});

Typed Arrays

向來 JavaScript 處理 Binary Data 都比較麻煩, Typed arrays 的出現就能夠使代碼快速處理這些數據。詳細代碼可以參照這裡

Generator

Generator 是一種函數,而這一種函數可以中途離開,下一次進入的時候則會載入上一次離開時的狀態(變量)。跟函數不同的是當調用 Generator 的時候,是返回一個 iterator, 當執行這個 iterator.next() 的時候才會執行 Generator 所定義的函數直至第一個 yield, yield 定義了所 return 的值。以下是一個訂單編號產生器:

function * orderIndexGenerator () {
  var index = 1;
  var startDay = new Date().toISOString().substring(0, 10);
  while (true) {
    let today = new Date().toISOString().substring(0, 10);
    // 如果下一次呼叫 .next() 時候已經過了一天的話,就需要更新預設值,那就確保每一天的訂單都會從 1 開始
    if (startDay !== today) {
      startDay = today;
      index = 1;
    }
    yield startDay + '-' + index++;
  }
}
var oig = new oderIndexGenerator();
console.log(oig.next().value); // 2015-09-28-1
console.log(oig.next().value); // 2015-09-28-2
console.log(oig.next().value); // 2015-09-28-3
// 下一天再執行
console.log(oig.next().value); // 2015-09-29-1

Binary and Octal grammar

創建二進制數字的語法需要加上一個 leading zero, (0b 或 0B)。如果 0b 或 0B 後面的不是 0 或 1, 編譯時就會出現 SyntaxError 的錯誤。

var binaryNum = 0b3; // SyntaxError: Unexpected token ILLEGAL

同樣地創建八進制數字的語法需要加上一個 leading zero, (0o 或 0O)。如果 0o 或 0O 後面的不是 0,1,2,3,4,5,6,7, 編譯時就會出現 SyntaxError 的錯誤。

var octNum = 0b8; // SyntaxError: Unexpected token ILLEGAL

Extension for Object Literal

有經驗的開發者應該不難發現 ES6 的 Object 與先前提到的 class 十分相似,可以看看以下的代碼:

var protoObject = { key: 'value' };
var obj = {
    __proto__: protoObject,
    findSuperKey () {
        console.log(super.key); // 這裡的 super 就是指 __proto__
    }
};

換轉如果用 class 寫的話

'use strict';
class protoObject {
  constructor () {
    this.key = 'value';
  }
}

class obj extends protoObject {
  constructor () {
    super();
  }
  findSuperKey () {
    console.log(this.key);
  }
}

var o = new obj();
o.findSuperKey; // 'value'

另外 ES6 提供了一個快捷的方法去創建 Object, 就是如果當 Object key 的名稱跟變數的名稱是一樣的話,就可以縮短 Object 的代碼長度,減少冗餘。

// ES6 的語法糖
var a = 'apple', b = 'boy', c = 'cat';
var childrenVocab = {a, b, c};
// 以前的寫法
var a = 'apple', b = 'boy', c = 'cat';
var childrenVocab = { a: a, b: b, c: c };

Object 的鍵名也可以動態加入,不一定用 static string 來表示, 使代碼更容易擴展

var obj = {
  [(function(){return 'dymKey'})()] : 'dymKeyValue'
};
// { dymKey: 'dymKeyValue' }

New String methods

String.prototype.codePointAt
String.prototype.normalize
String.prototype.repeat
String.prototype.startsWith
String.prototype.endsWith
String.prototype.includes
String.prototype[Symbol.iterator]
// static methods
String.raw
String.fromCodePoint

Symbols

Symbol 是 ES6 所定義的第七種 JavaScript 基本類型,是一種不可變的數據型別,是對原始數據的封裝。

// 1. 基本應用,封裝原始數據,支援 typeof
var s = Symbol();
var s = Symbol('foo');
var s = Symbol(12);
var s = Symbol({ a: 1 });
typeof Symbol(); // 'symbol'

// 2. 使用 new 語法會拋出 TypeError 錯誤
var s = new Symbol(); // TypeError

// 3. 不能轉換成 string, number 或使用 JSON.stringify
var s = Symbol('foo');
s + 0; // TypeError: Cannot convert a Symbol value to a number
s + 'foo'; // TypeError: Cannot convert a Symbol value to a string

// 4. 每次創建都是新的
Symbol('foo') === Symbol('foo'); // false

// 5. 能夠封裝成 String
String(Symbol('foo')); // 'Symbol(foo)'

// 6. 可以用作 Object 的 key
var obj = {};
var sym = Symbol();
obj[sym] = 1;
console.log(obj[sym]); // 1
// 為了避免與 string key 有衝突, .keys 以及 .getOwnPropertyNames 均不會訪問得到 Symbol key
Object.getOwnPropertyNames(obj); // []
Object.keys(obj); // []
Object.getOwnPropertySymbols(obj); // [ Symbol() ]

// 7. 註冊表
var symbol = Symbol.for('foo');
Symbol.for('foo') === symbol && Symbol.keyFor(symbol) === 'foo'; // true

實際應用場景可以看看這裡

Template strings

簡單而言,這是一種語法糖,定義了多行字串(multi-lined string)的寫法,加入了以及加入標籤。

// Before ES6
var ms = 'A new line is then inserted.\nI am in the new line!';
// ES6 syntax sugar
var ms = `A new line is then inserted.
I am in the new line!`

// 模板字符串
var a = 1;
var b = 1;
// Before ES6
console.log(a + ' + ' + b + ' equals to ' + (a+b));
// ES6
console.log(`${a} + ${b} equals to ${a+b}`);

不過這裡會衍生安全性問題,由於 ${...} 的寫法可以訪問變量內容,所以不能夠直接用作處理用戶端的輸入。

Arrow Functions

這並不是一種新的概念,這種匿名函數其實一直都在使用。

// Before ES6
var helloTargets = ['Alice','Bob','Cindy'];
helloTargets.map(function(target){
  console.log('Hello ' + target);
});
// Hello Alice
// Hello Bob
// Hello Cindy

// ES6 的代碼變得更簡潔
var helloTargets = ['Alice', 'Bob', 'Cindy'];
helloTargets.map((target) => console.log('Hello ' + target));

// 第二個例子
var psyTest = age => doingTest(age);
var psyTest = (age) => doingTest(age);
// 用 age => 或 (age) => 都是一樣效果
var psyTest = age,job => doingTest(age,job); // SyntaxError: Unexpected token =>
var psyTest = (age,job) => doingTest(age,job);
// 如果沒有括號是有問題的,建議使用 (age) => 是為了方便日後代碼擴展

另外, =>function 有以下的不同:

  • 傳統的 function 可以利用 new 來構造, => 生成的函數就不能用 new 來構造。
  • Lexical this, super, arguments, new.target
  • .bind 對於 => 是無效的

由此可見,它省卻了寫 this 的麻煩與迷思。

// Before ES6
function mother(){
  this.isAngry = true;
  this.callSonToDoHouseWork(function (){
    if(this.isAngry){ // undefined
      this.shopping();
    }
  });
}

// 需要定義 self 去解決這個問題
function mother(){
  this.isAngry = true;
  var self = this;
  this.callSonToDoHouseWork(() => {
    if(self.isAngry){ // true
      self.shopping();
    }
  });
}

// => with lexical this,不需要定義 self
function mother(){
  this.isAngry = true;
  this.callSonToDoHouseWork(() => {
    if(this.isAngry){ // true
      this.shopping();
    }
  });
}

Promises

相信有寫過異步代碼 (Asynchronous) 的開發者對 Promise 應該不會陌生。它對於簡化代碼,解決 Callback hell, try/catch 無法抓到回調異常 (callback exception) 的問題的效果十分顯著。在先前的 Node.js 版本 (0.12) 已經有原生支持,當然還可以透過基於 Prmoises/A+ 標準所開發的第三方框架去實作起來, (例如 Qbluebird 等)。

ES6 所定義的 Promise 有 4 種狀態, 分別是 Pending(待定), Fulfilled(成功完成), Rejected(失敗), Settled(已經完成/失敗)。

// 基本語法
new Promise(function(resolve, reject) { ... });

// 例子
var p1 = new Promise(function(resolve, reject){
  resolve('finished'); // resolve 就是 fullfil promise !
});
var p2 = new Promise(function(resolve, reject){
  reject('exception p2'); // reject 就是 reject promise !
});

p1
.then(function(val){
  // .then 定義當 promise 被 fulfil 時應做什麼
  // 這個時候的狀態就是 settled
  console.log(val); // 'finished'
});

p2
.then(function(val){
  // 這個時候的狀態就是 settled
  console.log(val);
})
.catch(function(result){
  // 這個時候的狀態就是 settled
  // .catch 定義當 promise 被 reject 時應做什麼
  console.log(result); // 'exception p2'
});

除了 .then.catch 外,還有 .all 以及 .race 的方法,這個文檔暫時只提供基本 Promise 的應用而已。

for...of loops

這是一個語法糖,類似 C# 裡面的 foreach(var item in items)

let i1 = [1,2,3];
for(let i of i1){
  console.log(i);
}
/*
1
2
3
*/
let i2 = 'abc';
for(let i of i2){
  console.log(i);
}
/*
a
b
c
*/

此外,for...of 迴圈還支援下列樣式

// Generator instance
for(let i of (function*(){ yield 1; yield 2; yield 3; }())) { ... }
// Generic iterable
for(let i of global.__createIterableObject([1, 2, 3])) { ... }
// Generic iterable instance
for(let i of Object.create(global.__createIterableObject([1, 2, 3]))) { ... }

結語

由於 ES6 剛發佈不久,不論前端或後端還是需要一定時間去調試和整合,大家可以去比較不同平台和瀏覽器目前的兼容性,這個文檔也會不停的更新。

參考資料