最近我參加了一個在舊金山舉行的HTML5開發者大會,聽到的演講半數都關於ES6(或者官方說法叫ECMAScript2015),我喜歡簡潔的成為ES6。
這篇文章會給你簡單介紹一下ES6。如果你還不知道什麼是ES6的話,它是JavaScript一個新的實現,如果你是一個忙碌的JavaScript開發者(但誰不是呢),那麼繼續讀下去吧,看看當今最熱門的語言——JavaScript的新一代實現中,最棒的十個特性。
這是為忙碌的開發者準備的ES6中最棒的十個特性(無特定順序):
- 預設引數
- 模版表示式
- 多行字串
- 拆包表示式
- 改進的物件表示式
- 箭頭函式
=&>
- Promise
- 塊級作用域的
let
和const
- 類
- 模組化
注意:這個列表十分主觀並且帶有偏見,其他未上榜的特性並不是因為沒有作用,我這麼做只是單純的希望將這份列表中的專案保持在十個。
首先,一個簡單的JavaScript時間線,不瞭解歷史的人也無法創造歷史。
- 1995年:JavaScript以LiveScript之名誕生
- 1997年:ECMAScript標準確立
- 1999年:ES3釋出,IE5非常生氣
- 2000年-2005年:
XMLHttpRequest
,熟知為AJAX
,在如Outlook Web Access(2002)、Oddpost(2002)、Gmail(2004)、Google Maps(2005)中得到了廣泛的應用 - 2009年:ES5釋出(這是我們目前用的最多的版本),帶來了
forEach
/Object.keys
/Object.create
(特地為Douglas Crockford所造,JSON標準建立者) ,還有JSON標準。
歷史課上完了,我們回來講程式設計。
1. ES6中的預設引數
還記得我們以前要這樣子來定義預設引數:
1 2 3 4 5 6 |
var link = function (height, color, url) { var height = height || 50 var color = color || 'red' var url = url || 'http://azat.co' ... } |
這樣做一直都沒什麼問題,直到引數的值為0
,因為0
在JavaScript中算是false
值,它會直接變成後面硬編碼的值而不是0
本身。當然了,誰要用0
來傳值啊(諷刺臉)?所以我們也忽略了這個瑕疵,沿用了這個邏輯,否則的話只能…..沒有否則!在ES6中,我們可以把這些預設值直接放在函式簽名中。
1 2 3 |
var link = function(height = 50, color = 'red', url = 'http://azat.co') { ... } |
對了,這個語法和Ruby很像!
2. ES6中的模版表示式
模版表示式在其他語言中一般是為了在模版字串中輸出變數,所以在ES5中,我們非得把字串破開變成這樣:
1 2 |
var name = 'Your name is ' + first + ' ' + last + '.' var url = 'http://localhost:3000/api/messages/' + id |
幸運的是在ES6中我們有了新語法,在反引號包裹的字串中,使用${NAME}語法來表示模板字元:
1 2 |
var name = `Your name is ${first} ${last}` var url = `http://localhost:3000/api/messages/${id}` |
3. ES6中的多行字串
另一個好吃的語法糖就是多行字串,以前我們的實現是像這樣的:
1 2 3 4 5 6 7 8 |
var roadPoem = 'Then took the other, as just as fair,nt' + 'And having perhaps the better claimnt' + 'Because it was grassy and wanted wear,nt' + 'Though as for that the passing therent' + 'Had worn them really about the same,nt' var fourAgreements = 'You have the right to be you.n You can only be you when you do your best.' |
但是在ES6中,只要充分利用反引號。
1 2 3 4 5 6 7 8 |
var roadPoem = `Then took the other, as just as fair, And having perhaps the better claim Because it was grassy and wanted wear, Though as for that the passing there Had worn them really about the same,` var fourAgreements = `You have the right to be you. You can only be you when you do your best.` |
4. ES6中的拆包表示式
拆包可能是一個比較難理解的概念,因為這裡面真的是有魔法發生。假如說你有一個簡單的賦值表示式,把物件中的house
的mouse
賦值為house
和mouse
的變數。
1 2 3 |
var data = $('body').data(), // 假設data中有mouse和house的值 house = data.house, mouse = data.mouse |
另一個拆包的例項(Node.js):
1 2 3 4 5 |
var jsonMiddleware = require('body-parser').json var body = req.body, // body中有使用者名稱和密碼值 username = body.username, password = body.password |
但是在ES6中我們可以用以下語句替換:
1 2 3 4 5 |
var { house, mouse} = $('body').data() // 我們會拿到house和mouse的值的 var {jsonMiddleware} = require('body-parser') var {username, password} = req.body |
甚至在陣列中也能用,簡直瘋狂!
1 2 |
var [col1, col2] = $('.column'), [line1, line2, line3, , line5] = file.split('n') |
習慣拆包語法可能需要一些時間,但是這絕對是糖衣炮彈。
5. ES6中改進的物件表示式
你能用物件表示式所做的是超乎想象的!類定義的方法從ES5中一個美化版的JSON,進化到ES6中更像類的構造。
這是一個ES5中典型的物件表示式,定義了一些方法和屬性。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var serviceBase = {port: 3000, url: 'azat.co'}, getAccounts = function(){return [1,2,3]} var accountServiceES5 = { port: serviceBase.port, url: serviceBase.url, getAccounts: getAccounts, toString: function() { return JSON.stringify(this.valueOf()) }, getUrl: function() {return 'http://' + this.url + ':' + this.port}, valueOf_1_2_3: getAccounts() } |
如果你想做的好看一點,我們可以用Object.create
方法來讓 serviceBase
成為 accountServiceES5
的 prototype
從而實現繼承。
1 2 3 4 5 6 7 8 9 |
var accountServiceES5ObjectCreate = Object.create(serviceBase) var accountServiceES5ObjectCreate = { getAccounts: getAccounts, toString: function() { return JSON.stringify(this.valueOf()) }, getUrl: function() {return 'http://' + this.url + ':' + this.port}, valueOf_1_2_3: getAccounts() } |
我知道 accountServiceES5ObjectCreate
和 accountServiceES5
是不完全相同的。因為一個物件 accountServiceES5
會有如下所示的 __proto__
屬性:
但對於這個示例,我們就把這兩者考慮為相同的。所以在ES6的物件表示式中,我們把getAccounts: getAccounts
簡化為getAccounts,
,並且我們還可以用__proto__
直接設定prototype
,這樣聽起來合理的多。(不過並不是用proto
)
1 2 3 4 5 |
var serviceBase = {port: 3000, url: 'azat.co'}, getAccounts = function(){return [1,2,3]} var accountService = { __proto__: serviceBase, getAccounts, |
還有,我們可以呼叫 super
和動態索引(valueOf_1_2_3
)
1 2 3 4 5 6 7 8 |
// 續上段程式碼 toString() { return JSON.stringify((super.valueOf())) }, getUrl() {return 'http://' + this.url + ':' + this.port}, [ 'valueOf_' + getAccounts().join('_') ]: getAccounts() }; console.log(accountService) |
這是對老舊的物件表示式一個很大的改進!
6. ES6中的箭頭函式
這或許是我最想要的一個特性,我愛 CoffeeScript 就是因為他胖胖的箭頭(=&>
相對於-&>
),現在ES6中也有了。這些箭頭最神奇的地方在於他會讓你寫正確的程式碼。比如,this
在上下文和函式中的值應當是相同的,它不會變化,通常變化的原因都是因為你建立了閉包。
使用箭頭函式可以讓我們不再用that = this
或者self = this
或者_this = this
或者.bind(this)
這樣的程式碼,比如,這些程式碼在ES5中就特別醜。
1 2 3 4 |
var _this = this $('.btn').click(function(event){ _this.sendData() }) |
這是在ES6中去掉_this = this
之後:
1 2 3 |
$('.btn').click((event) =>{ this.sendData() }) |
可惜的是,ES6委員會覺得再加上瘦箭頭(-&>
)的話就對我們太好了,所以他們留下了一個老舊的function
。(瘦箭頭在CoffeeScript中的作用就像ES5/6中一樣)
1 2 3 4 5 6 7 8 9 10 |
var logUpperCase = function() { var _this = this this.string = this.string.toUpperCase() return function () { return console.log(_this.string) } } logUpperCase.call({ string: 'es6 rocks' })() |
在ES6中我們無需_this
1 2 3 4 5 6 |
var logUpperCase = function() { this.string = this.string.toUpperCase() return () => console.log(this.string) } logUpperCase.call({ string: 'es6 rocks' })() |
注意,在ES6中你可以合理的把箭頭函式和舊式 function
函式混用。當箭頭函式所在語句只有一行時,它就會變成一個表示式,它會直接返回這個語句的值。但是如果你有多行語句,你就要明確的使用return
。
這是ES5中利用messages
陣列建立一個陣列的程式碼:
1 2 3 4 |
var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9'] var messages = ids.map(function (value) { return 'ID is ' + value // 顯式返回 }); |
在ES6中會變成這樣:
1 2 |
var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9'] var messages = ids.map(value => `ID is ${value}`) // 隱式返回 |
注意到我用了字串模版嗎,又一個從CoffeeScript中來的功能,我愛它們!
在只有一個引數的函式簽名中,括號是可有可無的,但是如果多於一個引數時就要加上。
1 2 |
var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9'] var messages = ids.map((value, index, list) => `ID of ${index} element is ${value} `) // 隱式返回 |
7. ES6中的Promise
Promise是一個有爭議的話題。現在有很多Promise實現,語法也大致相同,比如q
/ bluebird
/ deferred.js
/ vow
/ avow
/ jquery deferred
等等。其他人說我們並不需要Promise,非同步,回撥和generator
之類的就很好。慶幸的是,現在在ES6中終於有一個標準的Promise實現。
我們來看一個相當微不足道的延遲非同步執行,用setTimeout
實現
1 2 3 |
setTimeout(function(){ console.log('Yay!') }, 1000) |
我們可以用ES6中的Promise重寫:
1 2 3 4 5 |
var wait1000 = new Promise(function(resolve, reject) { setTimeout(resolve, 1000) }).then(function() { console.log('Yay!') }) |
或者用ES6的箭頭函式:
1 2 3 4 5 |
var wait1000 = new Promise((resolve, reject)=> { setTimeout(resolve, 1000) }).then(()=> { console.log('Yay!') }) |
到現在為止,我們只是單純增加了程式碼的行數,還明顯沒有帶來任何好處,你說的對。但是如果我們有更多複雜的邏輯內嵌在setTimeout()
中的回撥時好處就來了:
1 2 3 4 5 6 |
setTimeout(function(){ console.log('Yay!') setTimeout(function(){ console.log('Wheeyee!') }, 1000) }, 1000) |
可以用ES6中的Promise重寫:
1 2 3 4 5 6 7 8 9 10 |
var wait1000 = ()=> new Promise((resolve, reject)=> {setTimeout(resolve, 1000)}) wait1000() .then(function() { console.log('Yay!') return wait1000() }) .then(function() { console.log('Wheeyee!') }); |
還是無法相信Promise比普通回撥要好?我也不信。我想一旦知道了回撥這個方法它就會在你腦中縈繞,額外的複雜的Promise也沒有必要存在了。
不論怎麼說,ES6中的Promise是為會欣賞的人準備的,Promise有一個不錯的失敗-捕捉
回撥機制,看看這篇文章吧,裡面有更多關於Promise的資訊。ES6 Promise介紹
8. 塊級作用域的let
和const
你可能早就聽過對ES6中的let
那些奇怪的傳說,我記得我第一次到倫敦時為那些TO LET牌子感到非常困惑。但是ES6中的let
和出租無關,這不算是語法糖,它很複雜。let
是一個更新的var
,可以讓你把變數作用域限制在當前塊裡。我們用{}
來定義塊,但是在ES5中這些花括號起不到任何作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function calculateTotalAmount (vip) { var amount = 0 if (vip) { var amount = 1 } { // 讓塊來的更瘋狂 var amount = 100 { var amount = 1000 } } return amount } console.log(calculateTotalAmount(true)) |
執行結果將會是1000
。天啊!這是多大的一個Bug。在ES6中,我們用let
來限制變數作用域為函式內。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function calculateTotalAmount (vip) { var amount = 0 // 或許應該用let, 但你可以混用 if (vip) { let amount = 1 // 第一個數量為 0 } { // 更多的塊 let amount = 100 // 第一個數量為 0 { let amount = 1000 // 第一個數量為 0 } } return amount } console.log(calculateTotalAmount(true)) |
執行結果是0
,因為在if
塊中也有let
。如果什麼都沒有的話(amount=1
),那麼結果將會是1
。
說到const
,事情就簡單多了。他僅僅產生是一個不可變的變數,並且他的作用域也像let
一樣只有塊級。為了演示,這裡有定義了一堆常量,並且由於作用域的原因,這些定義都是有效的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function calculateTotalAmount (vip) { const amount = 0 if (vip) { const amount = 1 } { // 更多的塊 const amount = 100 { const amount = 1000 } } return amount } console.log(calculateTotalAmount(true)) |
依我愚見,let
和const
讓這門語言變得更加複雜,沒有這些的時候我們只有一條路可以走,但是現在可以要考慮更多的情景。;-(
9. ES6中的類
如果你喜歡物件導向程式設計,那麼你會特別喜歡這個特性。他讓你編寫和繼承類時就跟在Facebook上發一個評論這麼簡單。
在ES5中,因為沒有class
關鍵字(但它是毫無作用的保留字),類的建立和使用是讓人十分痛苦的事情。更慘的是,很多偽類的實現像pseude-classical, classical, functional讓人越來越摸不著頭腦,為JavaScript的信仰戰爭火上澆油。
我不會給你展示在ES5中怎麼去編寫一個類(是啦是啦從物件可以衍生出來其他的類和物件),因為有太多方法去完成。我們直接看ES6的示例,告訴你ES6的類會用prototype
來實現而不是function
。現在有一個baseModel
類,其中我們可以定義建構函式和getName()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 |
class baseModel { constructor(options = {}, data = []) { // class constructor this.name = 'Base' this.url = 'http://azat.co/api' this.data = data this.options = options } getName() { // class method console.log(`Class name: ${this.name}`) } } |
注意到我給options
和data
用了預設引數,而且方法名再也不用加上function
或者:
了。還有一個很大的區別,你不能像建構函式裡面一樣向this.Name
指派值。怎麼說呢,和函式
有相同縮排的程式碼裡,你不能向name
賦值。如果有這個需要的話,在建構函式裡面完成。
使用NAME extends PARENT_NAME
語法,AccountModel
從baseModel
繼承而來。
1 2 |
class AccountModel extends baseModel { constructor(options, data) { |
呼叫父類建構函式時,只需帶上引數輕鬆的呼叫super()
方法。
1 2 3 4 |
super({private: true}, ['32113123123', '524214691']) //call the parent method with super this.name = 'Account Model' this.url +='/accounts/' } |
想要高階一點的話,你可以像這樣弄一個getter
方法,這樣accountsData
就會變成一個屬性。
1 2 3 4 5 |
get accountsData() { // 返回計算後的資料 // ... make XHR return this.data } } |
現在你要怎麼用這個魔咒,很簡單,就跟讓三歲小孩相信聖誕老人存在一樣。
1 2 3 |
let accounts = new AccountModel(5) accounts.getName() console.log('Data is %s', accounts.accountsData) |
如果好奇輸出結果的話:
1 2 |
Class name: Account Model Data is 32113123123,524214691 |
10. ES6中的模組化
你可能知道,ES6之前JavaScript並沒有對模組化有過原生的支援,人們想出來AMD
,RequireJS
,CommenJS
等等,現在終於有import
和export
運算子來實現了。
ES5中你會用script
標籤和IIFE(立即執行函式)
,或者是其他的像AMD
之類的庫,但是ES6中你可以用export
來暴露你的類。我是喜歡Node.js的人,所以我用和Node.js語法一樣的CommonJS
,然後用Browserfy來瀏覽器化。現在我們有一個port
變數和getAccounts
方法,在ES5中:
1 2 3 4 5 6 |
module.exports = { port: 3000, getAccounts: function() { ... } } |
在ES5的main.js
中,用require('模組')
來匯入:
1 2 |
var service = require('module.js') console.log(service.port) // 3000 |
但是在ES6中,我們用export
和import
。比如這是ES6中的module.js
檔案:
1 2 3 4 |
export var port = 3000 export function getAccounts(url) { ... } |
在需要引入的main.js
檔案中,可以用import {名稱} from '模組'
語法:
1 2 |
import {port, getAccounts} from 'module' console.log(port) // 3000 |
或者就直接在main.js
中引入所有的變數:
1 2 |
import * as service from 'module' console.log(service.port) // 3000 |
個人來說,我覺得這樣的模組化有些搞不懂。確實,這樣會更傳神一些 。但是Node.js中的模組不會馬上就改過來,瀏覽器和伺服器的程式碼最好是用同樣的標準,所以目前我還是會堅持CommonJS/Node.js
的方式。
目前來說瀏覽器對ES6的支援還遙遙無期(本文寫作時),所以你需要一些像jspm這樣的工具來用ES6的模組。
想要了解更多ES6中的模組化和例子的話,來看這篇文章,不管怎麼說,寫現代化的JavaScript吧!
怎麼樣可以在今天就用上ES6(Babel)
ES6標準已經敲定,但還未被所有瀏覽器支援(Firefox的ES6功能一覽),如果想馬上就用上ES6,需要一個像Babel這樣的編譯器。你可以把他當獨立工具用,也可以將他整合到構建系統裡,Babel對Gulp
,Grunt
和Webpack
都有對應的外掛。
安裝Gulp外掛示例:
1 |
$ npm install --save-dev gulp-babel |
在gulpfile.js
中,定義這麼一個任務,將src
目錄下的app.js
檔案編譯到build
目錄下:
1 2 3 4 5 6 7 8 |
var gulp = require('gulp'), babel = require('gulp-babel') gulp.task('build', function () { return gulp.src('src/app.js') .pipe(babel()) .pipe(gulp.dest('build')) }) |
Node.js和ES6
對於Node.js,你可以用構建工具或者直接用獨立模組babel-core
:
1 |
$ npm install --save-dev babel-core |
然後在Node.js中呼叫這個函式:
1 |
require('babel-core').transform(es5Code, options) |
ES6的一些總結
ES6中還有很多你可能都用不上(至少現在用不上)的可圈可點的特性,以下無特定順序:
Math
/Number
/String
/Array
/Object
中新的方法- 二進位制和八進位制資料型別
- 自動展開多餘引數
For of
迴圈(又見面了CoffeeScript)Symbols
- 尾部呼叫優化
generator
- 更新的資料結構(如
Map
和Set
)
和有些人吃了一片薯片就一發不可收拾的人一樣(再來一片嘛就一片),對於那些停不下來想要知道關於更多ES6相關資訊的成績優秀的同學,我準備了擴充套件閱讀: