使用ES6進行開發的思考

發表於2015-07-27

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.jsgeneratorsRuntime.js)或者想使用盡可能少的helper(Babel的externalHelpers配置),那麼需要按你的需求進一步縮減可使用的ES6特性,如MapSet這些就不應該使用。

語法增強類

Arrow function

Arrow functions是ES6在語法上提供的一個很好的特性,其特點有:

  • 語法更為簡潔了。
  • 文法上的固定this物件。

我們鼓勵在可用的場景下使用Arrow functions,並以此代替原有的function關鍵字。

當然Arrow functions並不是全能的,在一些特別的場景下並不十分適用,最為典型的是Arrow functions無法提供函式名稱,因此做遞迴併不方便。雖然可以使用Y combinator來實現函式式的遞迴,但其可讀性會有比較大的損失。

配合後文會提到的物件字面量增強,現在我們定義方法/函式會有多種方式,建議執行以下規範:

  • 所有的Arrow functions的引數均使用括號()包裹,即便只有一個引數:

  • 定義函式儘量使用Arrow functions,而不是function關鍵字:

除非當前場景不合適使用Arrow functions,如函式表示式需要自遞迴、需要執行時可變的this物件等。

  • 對於物件、類中的方法,使用增強的物件字面量:

增強的物件字面量

物件字面量的增強主要體現在3個方面:

可在物件中直接定義方法

我們推薦使用這種方式定義方法。

可使用通過計算得出的鍵值

我們推薦在需要的時候使用計算得出的鍵值,以便在一個語句中完成整個物件的宣告。

與當前Scope中同名變數的簡寫

我們並不推薦這樣的用法,這對可讀性並沒有什麼幫助。

模板字串

模板字串的主要作用有2個:

多行字串

從上面的程式碼中可以看出,實際使用多行字串時,對齊是個比較麻煩的事。如果let html這一行本身又有縮排,那麼會讓程式碼更為難受一些。

因此我們不推薦使用多行字串,必要時還是可以使用陣列和join('')配合,而生成HTML的場景我們應該儘量使用模板引擎。

字串變數替換

這是一個非常方便的功能,我們鼓勵使用。但需要注意這些變數並不會被HTML轉義,所以在需要HTML轉義的場景,還是乖乖使用模板引擎或者其它的模板函式。

解構

解構(原諒我沒什麼好的翻譯)是個比較複雜的語法,比如:

還可以有更復雜的,具體可以參考MDN的文件

對於這樣一個複雜且多變的語法,我們要有選擇地使用,建議遵循以下原則:

  • 不要一次通過解構定義過多的變數,建議不要超過5個。
  • 謹慎在解構中使用“剩餘”功能,即let [foo, bar, ...rest] = getValue()這種方式。
  • 不要在物件解構中使用過深層級,建議不要超過2層。

函式引數增強

ES6為函式引數提供了預設值、剩餘引數等功能,同時在呼叫函式時允許將陣列展開為引數,如:

我們鼓勵使用這些特性讓函式的宣告和呼叫變得更為簡潔,但有一些細節需要注意:

  • 在使用預設引數時,如果引數預設值是固定且不會修改的,建議使用一個常量來作為預設值,避免每一次生成的開銷。
  • 不要對arguments物件使用展開運算,這不是一個陣列。

關鍵字類

let和const

這是2個用來定義變數的關鍵字,眾所周知的,let表示塊作用域的變數,而const表示常量。

需要注意的是,const僅表示這個變數不能被再將賦值,但並不表示變數是物件、陣列時其內容不能改變。如果需要一個不能改變內容的物件、陣列,使用Object.freeze方法定義一個真正的常量:

不過如果你在程式中能控制不修改物件的話,這並不具備什麼意義,Object.freeze是否會引起執行引擎的進一步優化也尚未得到證實。

我們推薦使用let全面替代var。同時建議僅在邏輯上是常量的情況下使用const,不要任何不會被二次賦值的場景均使用const

迭代器和for..of

迭代器是個好東西,至少我們可以很簡單地遍歷陣列了:

但是迭代器本身存在一些細微的缺點:

  • 效能稍微差了一些,對於陣列來說大致與Array.prototype.forEach相當,比不過原生的for迴圈。
  • 不能在迴圈體中得到索引i的值,因此如果需要索引則只能用原生的for迴圈。
  • 判斷一個物件是否可迭代比較煩人,沒有原生方法提供,需要自行使用typeof o[Symbol.iterator] === 'function'判斷。

對於迭代器,我們鼓勵使用並代替原生for迴圈,且推薦關注以下原則:

  • 對於僅一個語句的迴圈操作,建議使用forEach方法,配合Arrow functions可非常簡單地在一行寫下迴圈邏輯。
  • 對於多個語句的迴圈操作,建議使用for..of迴圈。
  • 對於迴圈的場景,需要注意非陣列但可迭代的物件,如MapSet等,因此除arguments這類物件外,均建議直接判斷是否可迭代,而不是length屬性。

生成器

生成器(Generators)也是一個比較複雜的功能,具體可以參考MDN的文件

對於生成器,我的建議是非常謹慎地使用,理由如下:

  • 生成器不是用來寫非同步的,雖然他確實有這樣一個效果,但這僅僅是一種Hack。非同步在未來一定是屬於asyncawait這兩個關鍵字的,但太多人眼裡生成器就是寫非同步用的,這會導致濫用。
  • 生成器經過Babel轉換後生成的程式碼較多,同時還需要generatorsRuntime庫的支援,成本較高。
  • 我們實際寫應用的大部分場景下暫時用不到。

生成器最典型的應用可以參考C#的LINQ獲取一些經驗,將對一個陣列的多次操作合併為一個迴圈是其最大的貢獻。

模組和模組載入器

ES6終於在語言層面上定義了模組的語法,但這並不代表我們現在可以使用ES6的模組,因為實際在ES6定稿的時候,它把模組載入器的規範給移除了。因此我們現在有的僅僅是一個模組的importexport語法,但具體如“模組名如何對應到URL”、“如何非同步/同步載入模組”、“如何按需載入模組”等這些均沒有明確的定義。

因此,在模組這一塊,我們的建議是使用標準語法書寫模組,但使用AMD作為執行時模組解決方案,其特點有:

  • 保持使用importexport進行模組的引入和定義,可以安全地使用命名export和預設export
  • 在使用Babel轉換時,配置modules: 'amd'轉換為AMD的模組定義。
  • 假定模組的URL解析是AMD的標準,import對應的模組名均以AMD標準書寫。
  • 不要依賴SystemJS這樣的ES6模組載入器。

這雖然很可能導致真正模組載入器規範定型後,我們的import模組路徑是不規範的。但出於ES6的模組不配合HTTP/2簡直沒法完的考慮,AMD一定很長一段時間內持續存在,我們的應用基本上都是等不到HTTP/2實際可用的日子的,所以無需擔心。

型別增強類

Unicode支援

這個東西基本沒什麼影響,我們很少遇到這些情況且已經習慣了這些情況,所以可以認為這個特性不存在而繼續開發。

Map和Set

兩個非常有用的型別,但對不少開發者來說,會困惑於其跟普通物件的區別,畢竟我們已經拿普通物件當MapSet玩了這麼多年了,也很少自己寫一個型別出來。

對於此,我們的建議是:

  • 當你的元素或者鍵值有可能不是字串時,無條件地使用MapSet
  • 有移除操作的需求時,使用MapSet
  • 當僅需要一個不可重複的集合時,使用Set優先於普通物件,而不要使用{foo: true}這樣的物件。
  • 當需要遍歷功能時,使用MapSet,因為其可以簡單地使用for..of進行遍歷。

因此,事實上僅有一種情況我們會使用普通的物件,即使用普通物件來表達一個僅有增量Map,且這個Map的鍵值是字串。

另外,WeakMapWeakSet是沒有辦法模擬實現的,因此不要使用

Proxy

這不是一個可以模擬實現的功能,沒法用,因此不要使用Proxy

Symbol

Symbol最簡單的解釋是“可用於鍵值的物件”,最大的用處可能就是用來定義一些私有屬性了。

我們建議謹慎使用Symbol,如果你使用它來定義私有屬性,那麼請保持整個專案內是一致的,不要混用Symbol和閉包定義私有屬性等手段。

可繼承的內建型別

按照ES6的規範,內建型別如ArrayFunctionDate等都是可以繼承且沒有什麼坑的。但是我們的程式碼要跑在ES3-5的環境下,顯然這一特性是不能享受的。

Promise

這個真沒什麼好說的,即便不是ES6,我們也已經滿地用著Promise了。

建議所有非同步均使用Promise實現,以便在未來享受asyncawait關鍵字帶來的便攜性。

另外,雖然Babel可以轉換asyncawait的程式碼,但不建議使用,因為轉換出來的程式碼比較繁瑣,且依賴於generatorsRuntime

各內建型別的方法增強

Array.fromString.prototype.repeat等,這些方法都可以通過shim庫支援,因此放心使用即可。

二進位制和八進位制數字字面量

這個特性基本上是留給演算法一族用的,因此我們的建議是除非數字本身在二/八進位制下才有含義,否則不要使用

反射API

Reflect物件是ES6提供的反射物件,但其實沒有什麼方法是必要的。

其中的delete(name)has(name)方法相當於deletein運算子,而defineProperty等在Object上本身就有一套了,因此不建議使用該物件

尾遞迴

當作不存在就好了……

相關文章