ECMAScript6已經於近日進入了RC階段,而早在其處於社群討論時,我就開始一直在嘗試使用ES6進行開發的方案。在Babel推出後,基於ES6的開發也有了具體可執行的解決方案,無論是Build還是Debug都能得到很好的支援。
而在有了充足的環境、工具之後,我們面臨的是對ES6眾多新特性的選擇和分析,以便選取一個最佳的子集,讓我們可以享受ES6帶來的便利(減少程式碼量、提高可讀性等)的同時,也可以順利執行於當前以ES3-ES5為主的瀏覽器環境中。
經過分析後,本文試圖對ES6各個特性得出是否適合應用的初步結論,並一一解釋其使用場景。ES6的特性列表選自es6features。
- ★★★ 推薦使用
- ★★ 有考慮地使用
- ★ 慎重地使用
- ☆ 不使用
特性 | 推薦程度 |
---|---|
arrows | ★★★ |
classes | ★★★ |
enhanced object literals | ★★★ |
template strings | ★★★ |
destructuring | ★★ |
default + rest + spread | ★★★ |
let + const | ★★★ |
iterators + for..of | ★★ |
generators | ★ |
unicode | ☆ |
modules | ★★ |
module loaders | ☆ |
map + set + weakmap + weakset | ★★ |
proxies | ☆ |
symbols | ★ |
subclassable built-ins | ☆ |
promises | ★★★ |
math + number + string + array + object APIs | ★★★ |
binary and octal literals | ★ |
reflect api | ☆ |
tail calls | ★★ |
接下來我們以上特性挨個進行介紹。需要關注一點:如果你不想使用shim庫(如Babel的browser-polyfill.js
和generatorsRuntime.js
)或者想使用盡可能少的helper(Babel的externalHelpers
配置),那麼需要按你的需求進一步縮減可使用的ES6特性,如Map
、Set
這些就不應該使用。
語法增強類
Arrow function
Arrow functions是ES6在語法上提供的一個很好的特性,其特點有:
- 語法更為簡潔了。
- 文法上的固定
this
物件。
我們鼓勵在可用的場景下使用Arrow functions,並以此代替原有的function
關鍵字。
當然Arrow functions並不是全能的,在一些特別的場景下並不十分適用,最為典型的是Arrow functions無法提供函式名稱,因此做遞迴併不方便。雖然可以使用Y combinator來實現函式式的遞迴,但其可讀性會有比較大的損失。
配合後文會提到的物件字面量增強,現在我們定義方法/函式會有多種方式,建議執行以下規範:
- 所有的Arrow functions的引數均使用括號
()
包裹,即便只有一個引數:
1 2 3 4 5 |
// Good let foo = (x) => x + 1; // Bad let foo = x => x + 1; |
- 定義函式儘量使用Arrow functions,而不是
function
關鍵字:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Good let foo = { bar() { // code } }; // Bad let foo = { bar: () => { // code } }; // Bad let foo = { bar: function () { // code } }; |
除非當前場景不合適使用Arrow functions,如函式表示式需要自遞迴、需要執行時可變的this
物件等。
- 對於物件、類中的方法,使用增強的物件字面量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Good let foo = { bar() { // code } }; // Bad let foo = { bar: () => { // code } }; // Bad let foo = { bar: function () { // code } }; |
增強的物件字面量
物件字面量的增強主要體現在3個方面:
可在物件中直接定義方法
1 2 3 4 5 |
let foo = { bar() { // code } }; |
我們推薦使用這種方式定義方法。
可使用通過計算得出的鍵值
1 2 3 4 |
let MY_KEY = 'bar'; let foo = { [MY_KEY + 'Hash']: 123 }; |
我們推薦在需要的時候使用計算得出的鍵值,以便在一個語句中完成整個物件的宣告。
與當前Scope中同名變數的簡寫
1 2 3 4 |
let bar = 'bar'; let foo = { bar // 相當於bar: bar }; |
我們並不推薦這樣的用法,這對可讀性並沒有什麼幫助。
模板字串
模板字串的主要作用有2個:
多行字串
1 2 3 4 |
let html = `<div> <p>Hello World</p> </div>` |
從上面的程式碼中可以看出,實際使用多行字串時,對齊是個比較麻煩的事。如果let html
這一行本身又有縮排,那麼會讓程式碼更為難受一些。
因此我們不推薦使用多行字串,必要時還是可以使用陣列和join('')
配合,而生成HTML的場景我們應該儘量使用模板引擎。
字串變數替換
1 |
let message = `Hello ${name}, it's ${time} now`; |
這是一個非常方便的功能,我們鼓勵使用。但需要注意這些變數並不會被HTML轉義,所以在需要HTML轉義的場景,還是乖乖使用模板引擎或者其它的模板函式。
解構
解構(原諒我沒什麼好的翻譯)是個比較複雜的語法,比如:
1 2 |
let [foo, bar] = [1, 2]; let {id, name, children} = getTreeRoot(); |
還可以有更復雜的,具體可以參考MDN的文件。
對於這樣一個複雜且多變的語法,我們要有選擇地使用,建議遵循以下原則:
- 不要一次通過解構定義過多的變數,建議不要超過5個。
- 謹慎在解構中使用“剩餘”功能,即
let [foo, bar, ...rest] = getValue()
這種方式。 - 不要在物件解構中使用過深層級,建議不要超過2層。
函式引數增強
ES6為函式引數提供了預設值、剩餘引數等功能,同時在呼叫函式時允許將陣列展開為引數,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var foo = (x = 1) => x + 1; foo(); // 2 var extend = (source, ...args) => { for (let target in args) { for (let name in Object.keys(target) { if (!source.hasOwnProperty(name) { source[name] = target[name]; } } } }; var extensions = [ {name: 'Zhang'}, {age: 17}, {work: 'hard'} ]; extend({}, ...extensions); |
我們鼓勵使用這些特性讓函式的宣告和呼叫變得更為簡潔,但有一些細節需要注意:
- 在使用預設引數時,如果引數預設值是固定且不會修改的,建議使用一個常量來作為預設值,避免每一次生成的開銷。
- 不要對
arguments
物件使用展開運算,這不是一個陣列。
關鍵字類
let和const
這是2個用來定義變數的關鍵字,眾所周知的,let
表示塊作用域的變數,而const
表示常量。
需要注意的是,const
僅表示這個變數不能被再將賦值,但並不表示變數是物件、陣列時其內容不能改變。如果需要一個不能改變內容的物件、陣列,使用Object.freeze
方法定義一個真正的常量:
1 |
const DEFAULT_OPTIONS = Object.freeze({id: 0, name: 'unknown'}); |
不過如果你在程式中能控制不修改物件的話,這並不具備什麼意義,Object.freeze
是否會引起執行引擎的進一步優化也尚未得到證實。
我們推薦使用let
全面替代var
。同時建議僅在邏輯上是常量的情況下使用const
,不要任何不會被二次賦值的場景均使用const
。
迭代器和for..of
迭代器是個好東西,至少我們可以很簡單地遍歷陣列了:
1 2 3 |
for (let item in array) { // code } |
但是迭代器本身存在一些細微的缺點:
- 效能稍微差了一些,對於陣列來說大致與
Array.prototype.forEach
相當,比不過原生的for
迴圈。 - 不能在迴圈體中得到索引
i
的值,因此如果需要索引則只能用原生的for
迴圈。 - 判斷一個物件是否可迭代比較煩人,沒有原生方法提供,需要自行使用
typeof o[Symbol.iterator] === 'function'
判斷。
對於迭代器,我們鼓勵使用並代替原生for
迴圈,且推薦關注以下原則:
- 對於僅一個語句的迴圈操作,建議使用
forEach
方法,配合Arrow functions可非常簡單地在一行寫下迴圈邏輯。 - 對於多個語句的迴圈操作,建議使用
for..of
迴圈。 - 對於迴圈的場景,需要注意非陣列但可迭代的物件,如
Map
和Set
等,因此除arguments
這類物件外,均建議直接判斷是否可迭代,而不是length
屬性。
生成器
生成器(Generators)也是一個比較複雜的功能,具體可以參考MDN的文件。
對於生成器,我的建議是非常謹慎地使用,理由如下:
- 生成器不是用來寫非同步的,雖然他確實有這樣一個效果,但這僅僅是一種Hack。非同步在未來一定是屬於
async
和await
這兩個關鍵字的,但太多人眼裡生成器就是寫非同步用的,這會導致濫用。 - 生成器經過Babel轉換後生成的程式碼較多,同時還需要
generatorsRuntime
庫的支援,成本較高。 - 我們實際寫應用的大部分場景下暫時用不到。
生成器最典型的應用可以參考C#的LINQ獲取一些經驗,將對一個陣列的多次操作合併為一個迴圈是其最大的貢獻。
模組和模組載入器
ES6終於在語言層面上定義了模組的語法,但這並不代表我們現在可以使用ES6的模組,因為實際在ES6定稿的時候,它把模組載入器的規範給移除了。因此我們現在有的僅僅是一個模組的import
和export
語法,但具體如“模組名如何對應到URL”、“如何非同步/同步載入模組”、“如何按需載入模組”等這些均沒有明確的定義。
因此,在模組這一塊,我們的建議是使用標準語法書寫模組,但使用AMD作為執行時模組解決方案,其特點有:
- 保持使用
import
和export
進行模組的引入和定義,可以安全地使用命名export
和預設export
。 - 在使用Babel轉換時,配置
modules: 'amd'
轉換為AMD的模組定義。 - 假定模組的URL解析是AMD的標準,
import
對應的模組名均以AMD標準書寫。 - 不要依賴
SystemJS
這樣的ES6模組載入器。
這雖然很可能導致真正模組載入器規範定型後,我們的import
模組路徑是不規範的。但出於ES6的模組不配合HTTP/2簡直沒法完的考慮,AMD一定很長一段時間內持續存在,我們的應用基本上都是等不到HTTP/2實際可用的日子的,所以無需擔心。
型別增強類
Unicode支援
這個東西基本沒什麼影響,我們很少遇到這些情況且已經習慣了這些情況,所以可以認為這個特性不存在而繼續開發。
Map和Set
兩個非常有用的型別,但對不少開發者來說,會困惑於其跟普通物件的區別,畢竟我們已經拿普通物件當Map
和Set
玩了這麼多年了,也很少自己寫一個型別出來。
對於此,我們的建議是:
- 當你的元素或者鍵值有可能不是字串時,無條件地使用
Map
和Set
。 - 有移除操作的需求時,使用
Map
和Set
。 - 當僅需要一個不可重複的集合時,使用
Set
優先於普通物件,而不要使用{foo: true}
這樣的物件。 - 當需要遍歷功能時,使用
Map
和Set
,因為其可以簡單地使用for..of
進行遍歷。
因此,事實上僅有一種情況我們會使用普通的物件,即使用普通物件來表達一個僅有增量Map
,且這個Map
的鍵值是字串。
另外,WeakMap
和WeakSet
是沒有辦法模擬實現的,因此不要使用。
Proxy
這不是一個可以模擬實現的功能,沒法用,因此不要使用Proxy。
Symbol
Symbol
最簡單的解釋是“可用於鍵值的物件”,最大的用處可能就是用來定義一些私有屬性了。
我們建議謹慎使用Symbol
,如果你使用它來定義私有屬性,那麼請保持整個專案內是一致的,不要混用Symbol
和閉包定義私有屬性等手段。
可繼承的內建型別
按照ES6的規範,內建型別如Array
、Function
、Date
等都是可以繼承且沒有什麼坑的。但是我們的程式碼要跑在ES3-5的環境下,顯然這一特性是不能享受的。
Promise
這個真沒什麼好說的,即便不是ES6,我們也已經滿地用著Promise
了。
建議所有非同步均使用Promise實現,以便在未來享受async
和await
關鍵字帶來的便攜性。
另外,雖然Babel可以轉換async
和await
的程式碼,但不建議使用,因為轉換出來的程式碼比較繁瑣,且依賴於generatorsRuntime
。
各內建型別的方法增強
如Array.from
、String.prototype.repeat
等,這些方法都可以通過shim庫支援,因此放心使用即可。
二進位制和八進位制數字字面量
這個特性基本上是留給演算法一族用的,因此我們的建議是除非數字本身在二/八進位制下才有含義,否則不要使用。
反射API
Reflect
物件是ES6提供的反射物件,但其實沒有什麼方法是必要的。
其中的delete(name)
和has(name)
方法相當於delete
和in
運算子,而defineProperty
等在Object
上本身就有一套了,因此不建議使用該物件。
尾遞迴
當作不存在就好了……