作者:百度EFE – otakustay
網址:http://efe.baidu.com/blog/es6-develop-overview/?qq-pf-to=pcqq.c2c
點選“閱讀原文”可檢視本文網頁版
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的引數均使用括號()包裹,即便只有一個引數:
// Good
let foo = (x) => x + 1;
// Bad
let foo = x => x + 1;
- 定義函式儘量使用Arrow functions,而不是function關鍵字:
// Good
let foo = {
bar() {
// code
}
};
// Bad
let foo = {
bar: () => {
// code
}
};
// Bad
let foo = {
bar: function () {
// code
}
};
除非當前場景不合適使用Arrow functions,如函式運算式需要自遞迴、需要執行時可變的this物件等。
- 對於物件、類中的方法,使用增強的物件字面量:
// Good
let foo = {
bar() {
// code
}
};
// Bad
let foo = {
bar: () => {
// code
}
};
// Bad
let foo = {
bar: function () {
// code
}
};
增強的物件字面量
物件字面量的增強主要體現在3個方面:
可在物件中直接定義方法
let foo = {
bar() {
// code
}
};
我們推薦使用這種方式定義方法。
可使用透過計算得出的鍵值
let MY_KEY = ‘bar’;
let foo = {
[MY_KEY + ‘Hash’]: 123
};
我們推薦在需要的時候使用計算得出的鍵值,以便在一個陳述句中完成整個物件的宣告。
與當前Scope中同名變數的簡寫
let bar = ‘bar’;
let foo = {
bar // 相當於bar: bar
};
我們並不推薦這樣的用法,這對可讀性並沒有什麼幫助。
模板字串
模板字串的主要作用有2個:
多行字串
let html =
`
Hello World
`
從上面的程式碼中可以看出,實際使用多行字串時,對齊是個比較麻煩的事。如果let html這一行本身又有縮排,那麼會讓程式碼更為難受一些。
因此我們不推薦使用多行字串,必要時還是可以使用陣列和join(”)配合,而生成HTML的場景我們應該儘量使用模板引擎。
字串變數替換
let message = `Hello ${name}, it’s ${time} now`;
這是一個非常方便的功能,我們鼓勵使用。但需要註意這些變數並不會被HTML轉義,所以在需要HTML轉義的場景,還是乖乖使用模板引擎或者其它的模板函式。
解構
解構(原諒我沒什麼好的翻譯)是個比較複雜的語法,比如:
let [foo, bar] = [1, 2];
let {id, name, children} = getTreeRoot();
還可以有更複雜的,具體可以參考MDN的檔案。
對於這樣一個複雜且多變的語法,我們要有選擇地使用,建議遵循以下原則:
不要一次透過解構定義過多的變數,建議不要超過5個。
謹慎在解構中使用“剩餘”功能,即let [foo, bar, …rest] = getValue()這種方式。
不要在物件解構中使用過深層級,建議不要超過2層。
函式引數增強
ES6為函式引數提供了預設值、剩餘引數等功能,同時在呼叫函式時允許將陣列展開為引數,如:
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方法定義一個真正的常量:
const DEFAULT_OPTIONS = Object.freeze({id: 0, name: ‘unknown’});
不過如果你在程式中能控制不修改物件的話,這並不具備什麼意義,Object.freeze是否會引起執行引擎的進一步最佳化也尚未得到證實。
我們推薦使用let全面替代var。同時建議僅在邏輯上是常量的情況下使用const,不要任何不會被二次賦值的場景均使用const。
迭代器和for..of
迭代器是個好東西,至少我們可以很簡單地遍歷陣列了:
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上本身就有一套了,因此不建議使用該物件。
尾遞迴
當作不存在就好了……