給 JavaScript 初心者的 ES2015 實(shí)戰(zhàn)
前言
歷時(shí)將近6年的時(shí)間來制定的新 ECMAScript 標(biāo)準(zhǔn) ECMAScript 6(亦稱 ECMAScript Harmony,簡稱 ES6)終于在 2015 年 6 月正式發(fā)布。自從上一個(gè)標(biāo)準(zhǔn)版本 ES5 在 2009 年發(fā)布以后,ES6 就一直以新語法、新特性的優(yōu)越性吸引著眾多 JavaScript 開發(fā)者,驅(qū)使他們積極嘗鮮。
雖然至今各大瀏覽器廠商所開發(fā)的 JavaScript 引擎都還沒有完成對(duì) ES2015 中所有特性的完美支持,但這并不能阻擋工程師們對(duì) ES6 的熱情,于是乎如 babel、Traceur 等編譯器便出現(xiàn)了。它們能將尚未得到支持的 ES2015 特性轉(zhuǎn)換為 ES5 標(biāo)準(zhǔn)的代碼,使其得到瀏覽器的支持。其中,babel 因其模塊化轉(zhuǎn)換器(Transformer)的設(shè)計(jì)特點(diǎn)贏得了絕大部份 JavaScript 開發(fā)者的青睞,本文也將以 babel 為基礎(chǔ)工具,向大家展示 ES2015 的神奇魅力。
筆者目前所負(fù)責(zé)的項(xiàng)目中,已經(jīng)在前端和后端全方位的使用了 ES2015 標(biāo)準(zhǔn)進(jìn)行 JavaScript 開發(fā),已有將近兩年的 ES2015 開發(fā)經(jīng)驗(yàn)。如今 ES2015 以成為 ECMA 國際委員會(huì)的首要語言標(biāo)準(zhǔn),使用 ES2015 標(biāo)準(zhǔn)所進(jìn)行的工程開發(fā)已打好了堅(jiān)實(shí)的基礎(chǔ),而 ES7(ES2016) 的定制也走上了正軌,所以在這個(gè)如此恰當(dāng)?shù)臅r(shí)機(jī),我覺得應(yīng)該寫一篇通俗易懂的 ES2015 教程來引導(dǎo)廣大 JavaScript 愛好者和工程師向新時(shí)代前進(jìn)。若您能從本文中有所收獲,便是對(duì)我最大的鼓勵(lì)。
我希望你在閱讀本文前,已經(jīng)掌握了 JavaScript 的基本知識(shí),并具有一定的 Web App 開發(fā)基礎(chǔ)和 Node.js 基本使用經(jīng)驗(yàn)。
目錄
一言蔽之 ES2015
ES2015 能為 JavaScript 的開發(fā)帶來什么
- 語法糖
- 工程優(yōu)勢(shì)
ES2015 新語法詳解
let
、 const
和塊級(jí)作用域
箭頭函數(shù)(Arrow Function)
- 使用方法
- 箭頭函數(shù)與上下文綁定
- 注意事項(xiàng)
模板字符串
對(duì)象字面量擴(kuò)展語法
- 方法屬性省略
function
- 支持注入
__proto__
- 同名方法屬性省略語法
- 可以動(dòng)態(tài)計(jì)算的屬性名稱
表達(dá)式解構(gòu)
函數(shù)參數(shù)表達(dá)、傳參
默認(rèn)參數(shù)值
后續(xù)參數(shù)
解構(gòu)傳參
注意事項(xiàng)
新的數(shù)據(jù)結(jié)構(gòu)
- Set 和 WeakSet
- Map 和 WeakMap
類(Classes)
生成器(Generator)
來龍
基本概念
- Generator Function
- Generator
基本使用方法
Promise
原生的模塊化
Symbol
Proxy
(代理)
ES2015 的前端開發(fā)實(shí)戰(zhàn)
構(gòu)建界面
結(jié)構(gòu)定義
架構(gòu)設(shè)計(jì)
構(gòu)建應(yīng)用
入口文件
數(shù)據(jù)層:文章
路由:首頁
- 準(zhǔn)備頁面渲染
- 加載數(shù)據(jù)
- 設(shè)計(jì)組件
路由:文章頁面
路由:發(fā)布新文章
路由綁定
合并代碼
ES2015 的 Node.js 開發(fā)實(shí)戰(zhàn)
架構(gòu)設(shè)計(jì)
構(gòu)建應(yīng)用
入口文件
數(shù)據(jù)抽象層
Posts 控制器
- API:獲取所有文章
- API:獲取指定文章
- API:發(fā)布新文章
Comments 控制器
- API:獲取指定文章的評(píng)論
- API:發(fā)表新評(píng)論
配置路由
配置任務(wù)文件
部署到 DaoCloud
- Dockerfile
- 創(chuàng)建 DaoCloud 上的 MongoDB 服務(wù)
- 代碼構(gòu)建
一窺 ES7
async/await
Decorators
后記
本文的實(shí)戰(zhàn)部份將以開發(fā)一個(gè)動(dòng)態(tài)博客系統(tǒng)為背景,向大家展示如何使用 ES2015 進(jìn)行項(xiàng)目開發(fā)。成品代碼將在 GitHub 上展示。
一言蔽之 ES2015
說到 ES2015,有了解過的同學(xué)一定會(huì)馬上想到各種新語法,如箭頭函數(shù)(=>
)、class
、模板字符串等。是的,ECMA 委員會(huì)吸取了許多來自全球眾多 JavaScript 開發(fā)者的意見和來自其他優(yōu)秀編程語言的經(jīng)驗(yàn),致力于制定出一個(gè)更適合現(xiàn)代 JavaScript 開發(fā)的標(biāo)準(zhǔn),以達(dá)到“和諧”(Harmony)。一言蔽之:
ES2015 標(biāo)準(zhǔn)提供了許多新的語法和編程特性以提高 JavaScript 的開發(fā)效率和體驗(yàn)
從 ES6 的別名被定為 Harmony 開始,就注定了這個(gè)新的語言標(biāo)準(zhǔn)將以一種更優(yōu)雅的姿態(tài)展現(xiàn)出來,以適應(yīng)日趨復(fù)雜的應(yīng)用開發(fā)需求。
ES2015 能為 JavaScript 的開發(fā)帶來什么
語法糖
如果您有其他語言(如 Ruby、Scala)或是某些 JavaScript 的衍生語言(如 CoffeeScript、TypeScript)的開發(fā)經(jīng)驗(yàn),就一定會(huì)了解一些很有意思的語法糖,如 Ruby 中的 Range -> 1..10
,Scala 和 CoffeeScript 中的箭頭函數(shù) (a, b) => a + b
。ECMA 委員會(huì)借鑒了許多其他編程語言的標(biāo)準(zhǔn),給 ECMAScript 家族帶來了許多可用性非常高的語法糖,下文將會(huì)一一講解。
這些語法糖能讓 JavaScript 開發(fā)者更舒心地開發(fā) JavaScript 應(yīng)用,提高我們的工作效率~~,多一些時(shí)間出去浪~~。
工程優(yōu)勢(shì)
ES2015 除了提供了許多語法糖以外,還由官方解決了多年來困擾眾多 JavaScript 開發(fā)者的問題:JavaScript 的模塊化構(gòu)建。從許多年前開始,各大公司、團(tuán)隊(duì)、大牛都相繼給出了他們對(duì)于這個(gè)問題的不同解決方案,以至于定下了如 CommonJS、AMD、CMD 或是 UMD 等 JavaScript 模塊化標(biāo)準(zhǔn),RequireJS、SeaJS、FIS、Browserify、webpack 等模塊加載庫都以各自不同的優(yōu)勢(shì)占領(lǐng)著一方土地。
然而正正是因?yàn)檫@春秋戰(zhàn)國般的現(xiàn)狀,廣大的前端搬磚工們表示很納悶。
這?究竟哪種好?哪種適合我?求大神帶我飛!
對(duì)此,ECMA 委員會(huì)終于是坐不住了,站了起來表示不服,并制訂了 ES2015 的原生模塊加載器標(biāo)準(zhǔn)。
import fs from 'fs'
import readline from 'readline'
import path from 'path'
let Module = {
readLineInFile(filename, callback = noop, complete = noop) {
let rl = readline.createInterface({
input: fs.createReadStream(path.resolve(__dirname, './big_file.txt'))
})
rl.on('line', line => {
//... do something with the current line
callback(line)
})
rl.on('close', complete)
return rl
}
}
function noop() { return false }
export default Module
~~老實(shí)說,這套模塊化語法不禁讓我們又得要對(duì)那個(gè)很 silly 的問題進(jìn)行重新思考了:JavaScript 和 Java 有什么關(guān)系?~~
可惜的是,目前暫時(shí)還沒有任何瀏覽器廠商或是 JavaScript 引擎支持這種模塊化語法。所以我們需要用 babel 進(jìn)行轉(zhuǎn)換為 CommonJS、AMD 或是 UMD 等模塊化標(biāo)準(zhǔn)的語法。
ES2015 新語法詳解
經(jīng)過以上的介(xun)紹(tao),相信你對(duì) ES2015 也有了一定的了解和期待。接下來我將帶大家慢慢看看 ECMA 委員會(huì)含辛茹苦制定的新語言特性吧。
let
、const
和塊級(jí)作用域
在 ES2015 的新語法中,影響速度最為直接,范圍最大的,恐怕得數(shù) let
和 const
了,它們是繼 var
之后,新的變量定義方法。與 let
相比,const
更容易被理解:const
也就是 constant 的縮寫,跟 C/C++ 等經(jīng)典語言一樣,用于定義常量,即不可變量。
但由于在 ES6 之前的 ECMAScript 標(biāo)準(zhǔn)中,并沒有原生的實(shí)現(xiàn),所以在降級(jí)編譯中,會(huì)馬上進(jìn)行引用檢查,然后使用 var
代替。
// foo.js
const foo = 'bar'
foo = 'newvalue'
$ babel foo.js
...
SyntaxError: test.js: Line 3: "foo" is read-only
1 | const foo = 'bar'
2 |
> 3 | foo = 'newvalue'
...
塊級(jí)作用域
在 ES6 誕生之前,我們?cè)诮o JavaScript 新手解答困惑時(shí),經(jīng)常會(huì)提到一個(gè)觀點(diǎn):
JavaScript 沒有塊級(jí)作用域
在 ES6 誕生之前的時(shí)代中,JavaScript 確實(shí)是沒有塊級(jí)作用域的。這個(gè)問題之所以為人所熟知,是因?yàn)樗l(fā)了諸如歷遍監(jiān)聽事件需要使用閉包解決等問題。
<button>一</button>
<button>二</button>
<button>三</button>
<button>四</button>
<div id="output"></div>
<script>
var buttons = document.querySelectorAll('button')
var output = document.querySelector('#output')
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
output.innerText = buttons[i].innerText
})
}
</script>
前端新手非常容易寫出類似的代碼,因?yàn)閺闹庇^的角度看這段代碼并沒有語義上的錯(cuò)誤,但是當(dāng)我們點(diǎn)擊任意一個(gè)按鈕時(shí),就會(huì)報(bào)出這樣的錯(cuò)誤信息:
Uncaught TypeError: Cannot read property 'innerText' of undefined
出現(xiàn)這個(gè)錯(cuò)誤的原因是因?yàn)?buttons[i]
不存在,即為 undefined
。
為什么會(huì)出現(xiàn)按鈕不存在結(jié)果呢?通過排查,我們可以發(fā)現(xiàn),每次我們點(diǎn)擊按鈕時(shí),事件監(jiān)聽回調(diào)函數(shù)中得到的變量 i
都會(huì)等于 buttons.length
,也就是這里的 4。而 buttons[4]
恰恰不存在,所以導(dǎo)致了錯(cuò)誤的發(fā)生。
再而導(dǎo)致 i
得到的值都是 buttons.length
的原因就是因?yàn)?JavaScript 中沒有塊級(jí)作用域,而使對(duì) i
的變量引用(Reference)一直保持在上一層作用域(循環(huán)語句所在層)上,而當(dāng)循環(huán)結(jié)束時(shí) i
則正好是 buttons.length
。
而在 ES6 中,我們只需做出一個(gè)小小的改動(dòng),便可以解決該問題(假設(shè)所使用的瀏覽器已經(jīng)支持所需要的特性):
// ...
for (/* var */ let i = 0; i < buttons.length; i++) {
// ...
}
// ...
通過把 for
語句中對(duì)計(jì)數(shù)器 i
的定義語句從 var
換成 let
,即可。因?yàn)?let
語句會(huì)使該變量處于一個(gè)塊級(jí)作用域中,從而讓事件監(jiān)聽回調(diào)函數(shù)中的變量引用得到保持。我們不妨看看改進(jìn)后的代碼經(jīng)過 babel 的編譯會(huì)變成什么樣子:
// ...
var _loop = function (i) {
buttons[i].addEventListener('click', function () {
output.innerText = buttons[i].innerText
})
}
for (var i = 0; i < buttons.length; i++) {
_loop(i)
}
// ...
實(shí)現(xiàn)方法一目了然,通過傳值的方法防止了 i
的值錯(cuò)誤。
箭頭函數(shù)(Arrow Function)
繼 let
和 const
之后,箭頭函數(shù)就是使用率最高的新特性了。當(dāng)然了,如果你了解過 Scala 或者曾經(jīng)如日中天的 JavaScript 衍生語言 CoffeeScript,就會(huì)知道箭頭函數(shù)并非 ES6 獨(dú)創(chuàng)。
箭頭函數(shù),顧名思義便是使用箭頭(=>
)進(jìn)行定義的函數(shù),屬于匿名函數(shù)(Lambda)一類。當(dāng)然了,也可以作為定義式函數(shù)使用,但我們并不推薦這樣做,隨后會(huì)詳細(xì)解釋。
使用
箭頭函數(shù)有好幾種使用語法:
1. foo => foo + ' world' // means return `foo + ' world'`
2. (foo, bar) => foo + bar
3.
foo => {
return foo + ' world'
}
4.
(foo, bar) => {
return foo + bar
}
以上都是被支持的箭頭函數(shù)表達(dá)方式,其最大的好處便是簡潔明了,省略了 function
關(guān)鍵字,而使用 =>
代替。
箭頭函數(shù)語言簡潔的特點(diǎn)使其特別適合用於單行回調(diào)函數(shù)的定義:
let names = [ 'Will', 'Jack', 'Peter', 'Steve', 'John', 'Hugo', 'Mike' ]
let newSet = names
.map((name, index) => {
return {
id: index,
name: name
}
})
.filter(man => man.id % 2 == 0)
.map(man => [man.name])
.reduce((a, b) => a.concat(b))
console.log(newSet) //=> [ 'Will', 'Peter', 'John', 'Mike' ]
如果你有 Scala + Spark 的開發(fā)經(jīng)驗(yàn),就一定會(huì)覺得這非常親切,因?yàn)檫@跟其中的 RDD 操作幾乎如出一轍:
- 將原本的由名字組成的數(shù)組轉(zhuǎn)換為一個(gè)格式為
{ id, name }
的對(duì)象,id
則為每個(gè)名字在原數(shù)組中的位置
- 剔除其中
id
為奇數(shù)的元素,只保留 id
為偶數(shù)的元素
- 將剩下的元素轉(zhuǎn)換為一個(gè)包含當(dāng)前元素中原名字的單元數(shù)組,以方便下一步的處理
- 通過不斷合并相鄰的兩個(gè)數(shù)組,最后能得到的一個(gè)數(shù)組,便是我們需要得到的目標(biāo)值
箭頭函數(shù)與上下文綁定
事實(shí)上,箭頭函數(shù)在 ES2015 標(biāo)準(zhǔn)中,并不只是作為一種新的語法出現(xiàn)。就如同它在 CoffeeScript 中的定義一般,是用于對(duì)函數(shù)內(nèi)部的上下文 (this
)綁定為定義函數(shù)所在的作用域的上下文。
let obj = {
hello: 'world',
foo() {
let bar = () => {
return this.hello
}
return bar
}
}
window.hello = 'ES6'
window.bar = obj.foo()
window.bar() //=> 'world'
上面代碼中的 obj.foo
等價(jià)于:
// ...
foo() {
let bar = (function() {
return this.hello
}).bind(this)
return bar
}
// ...
為什么要為箭頭函數(shù)給予這樣的特性呢?我們可以假設(shè)出這樣的一個(gè)應(yīng)用場(chǎng)景,我們需要?jiǎng)?chuàng)建一個(gè)實(shí)例,用于對(duì)一些數(shù)據(jù)進(jìn)行查詢和篩選。
let DataCenter = {
baseUrl: 'http:///api/data',
search(query) {
fetch(`${this.baseUrl}/search?query=${query}`)
.then(res => res.json())
.then(rows => {
// TODO
})
}
}
此時(shí),從服務(wù)器獲得數(shù)據(jù)是一個(gè) JSON 編碼的數(shù)組,其中包含的元素是若干元素的 ID,我們需要另外請(qǐng)求服務(wù)器的其他 API 以獲得元素本身(當(dāng)然了,實(shí)際上的 API 設(shè)計(jì)大部份不會(huì)這么使用這么蛋疼的設(shè)計(jì))。我們就需要在回調(diào)函數(shù)中再次使用 this.baseUrl
這個(gè)屬性,如果要同時(shí)兼顧代碼的可閱讀性和美觀性,ES2015 允許我們這樣做。
let DataCenter = {
baseUrl: 'http:///api/data',
search(query) {
return fetch(`${this.baseUrl}/search?query=${query}`)
.then(res => res.json())
.then(rows => {
return fetch(`${this.baseUrl}/fetch?ids=${rows.join(',')}`)
// 此處的 this 是 DataCenter,而不是 fetch 中的某個(gè)實(shí)例
})
.then(res => res.json())
}
}
DataCenter.search('iwillwen')
.then(rows => console.log(rows))
因?yàn)樵趩涡心涿瘮?shù)中,如果 this
指向的是該函數(shù)的上下文,就會(huì)不符合直觀的語義表達(dá)。
注意事項(xiàng)
另外,要注意的是,箭頭函數(shù)對(duì)上下文的綁定是強(qiáng)制性的,無法通過 apply
或 call
方法改變其上下文。
let a = {
init() {
this.bar = () => this.dam
},
dam: 'hei',
foo() {
return this.dam
}
}
let b = {
dam: 'ha'
}
a.init()
console.log(a.foo()) //=> hei
console.log(a.foo.bind(b).call(a)) //=> ha
console.log(a.bar.call(b)) //=> hei
另外,因?yàn)榧^函數(shù)會(huì)綁定上下文的特性,故不能隨意在頂層作用域使用箭頭函數(shù),以防出錯(cuò):
// 假設(shè)當(dāng)前運(yùn)行環(huán)境為瀏覽器,故頂層作上下文為 `window`
let obj = {
msg: 'pong',
ping: () => {
return this.msg // Warning!
}
}
obj.ping() //=> undefined
let msg = 'bang!'
obj.ping() //=> bang!
為什么上面這段代碼會(huì)如此讓人費(fèi)解呢?
我們來看看它的等價(jià)代碼吧。
let obj = {
// ...
ping: (function() {
return this.msg // Warning!
}).bind(this)
}
// 同樣等價(jià)于
let obj = { /* ... */ }
obj.ping = (function() {
return this.msg
}).bind(this /* this -> window */)
模板字符串
模板字符串模板出現(xiàn)簡直對(duì) Node.js 應(yīng)用的開發(fā)和 Node.js 自身的發(fā)展起到了相當(dāng)大的推動(dòng)作用!我的意思并不是說這個(gè)原生的模板字符串能代替現(xiàn)有的模板引擎,而是說它的出現(xiàn)可以讓非常多的字符串使用變得尤為輕松。
模板字符串要求使用 ` 代替原本的單/雙引號(hào)來包裹字符串內(nèi)容。它有兩大特點(diǎn):
- 支持變量注入
- 支持換行
支持變量注入
模板字符串之所以稱之為“模板”,就是因?yàn)樗试S我們?cè)谧址幸猛獠孔兞?,而不需要像以往需要不斷地相加、相加、相加…?/p>
let name = 'Will Wen Gunn'
let title = 'Founder'
let company = 'LikMoon Creation'
let greet = `Hi, I'm ${name}, I am the ${title} at ${company}`
console.log(greet) //=> Hi, I'm Will Wen Gunn, I am the Founder at LikMoon Creation
支持換行
在 Node.js 中,如果我們沒有支持換行的模板字符串,若需要拼接一條SQL,則很有可能是這樣的:
var sql =
"SELECT * FROM Users " +
"WHERE FirstName='Mike' " +
"LIMIT 5;"
或者是這樣的:
var sql = [
"SELECT * FROM Users",
"WHERE FirstName='Mike'",
"LIMIT 5;"
].join(' ')
無論是上面的哪一種,都會(huì)讓我們感到很不爽。但若使用模板字符串,仿佛打開了新世界的大門~
let sql = `
SELECT * FROM Users
WHERE FirstName='Mike'
LIMIT 5;
`
Sweet! 在 Node.js 應(yīng)用的實(shí)際開發(fā)中,除了 SQL 的編寫,還有如 Lua 等嵌入語言的出現(xiàn)(如 Redis 中的 SCRIPT 命令),或是手工的 XML 拼接。模板字符串的出現(xiàn)使這些需求的解決變得不再糾結(jié)了~
對(duì)象字面量擴(kuò)展語法
看到這個(gè)標(biāo)題的時(shí)候,相信有很多同學(xué)會(huì)感到奇怪,對(duì)象字面量還有什么可以擴(kuò)展的?
確實(shí),對(duì)象字面量的語法在 ES2015 之前早已挺完善的了。不過,對(duì)于聰明的工程師們來說,細(xì)微的改變,也能帶來不少的價(jià)值。
方法屬性省略 function
這個(gè)新特性可以算是比較有用但并不是很顯眼的一個(gè)。
let obj = {
// before
foo: function() {
return 'foo'
},
// after
bar() {
return 'bar'
}
}
支持 __proto__
注入
在 ES2015 中,我們可以給一個(gè)對(duì)象硬生生的賦予其 __proto__
,這樣它就可以成為這個(gè)值所屬類的一個(gè)實(shí)例了。
class Foo {
constructor() {
this.pingMsg = 'pong'
}
ping() {
console.log(this.pingMsg)
}
}
let o = {
__proto__: new Foo()
}
o.ping() //=> pong
什么?有什么卵用?
有一個(gè)比較特殊的場(chǎng)景會(huì)需要用到:我想擴(kuò)展或者覆蓋一個(gè)類的方法,并生成一個(gè)實(shí)例,但覺得另外定義一個(gè)類就感覺浪費(fèi)了。那我可以這樣做:
let o = {
__proto__: new Foo(),
constructor() {
this.pingMsg = 'alive'
},
msg: 'bang',
yell() {
console.log(this.msg)
}
}
o.yell() //=> bang
o.ping() //=> alive
同名方法屬性省略語法
也是看上去有點(diǎn)雞肋的新特性,不過在做 JavaScript 模塊化工程的時(shí)候則有了用武之地。
// module.js
export default {
someMethod
}
function someMethod() {
// ...
}
// app.js
import Module from './module'
Module.someMethod()
可以動(dòng)態(tài)計(jì)算的屬性名稱
這個(gè)特性相當(dāng)有意思,也是可以用在一些特殊的場(chǎng)景中。
let arr = [1, 2, 3]
let outArr = arr.map(n => {
return {
[ n ]: n,
[ `${n}^2` ]: Math.pow(n, 2)
}
})
console.dir(outArr) //=>
[
{ '1': 1, '1^2': 1 },
{ '2': 2, '2^2': 4 },
{ '3': 3, '3^2': 9 }
]
在上面的兩個(gè) [...]
中,我演示了動(dòng)態(tài)計(jì)算的對(duì)象屬性名稱的使用,分別為對(duì)應(yīng)的對(duì)象定義了當(dāng)前計(jì)數(shù)器 n
和 n
的 2 次方
表達(dá)式解構(gòu)
來了來了來了,相當(dāng)有用的一個(gè)特性。有啥用?多重復(fù)值聽過沒?沒聽過?來看看吧!
// Matching with object
function search(query) {
// ...
// let users = [ ... ]
// let posts = [ ... ]
// ...
return {
users: users,
posts: posts
}
}
let { users, posts } = search('iwillwen')
// Matching with array
let [ x, y ] = [ 1, 2 ]
// missing one
[ x, ,y ] = [ 1, 2, 3 ]
function g({name: x}) {
console.log(x)
}
g({name: 5})
還有一些可用性不大,但也是有一點(diǎn)用處的:
// Fail-soft destructuring
var [a] = []
a === undefined //=> true
// Fail-soft destructuring with defaults
var [a = 1] = []
a === 1 //=> true
函數(shù)參數(shù)表達(dá)、傳參
這個(gè)特性有非常高的使用頻率,一個(gè)簡單的語法糖解決了從前需要一兩行代碼才能實(shí)現(xiàn)的功能。
默認(rèn)參數(shù)值
這個(gè)特性在類庫開發(fā)中相當(dāng)有用,比如實(shí)現(xiàn)一些可選參數(shù):
import fs from 'fs'
import readline from 'readline'
import path from 'path'
function readLineInFile(filename, callback = noop, complete = noop) {
let rl = readline.createInterface({
input: fs.createReadStream(path.resolve(__dirname, filename))
})
rl.on('line', line => {
//... do something with the current line
callback(line)
})
rl.on('close', complete)
return rl
}
function noop() { return false }
readLineInFile('big_file.txt', line => {
// ...
})
后續(xù)參數(shù)
我們知道,函數(shù)的 call
和 apply
在使用上的最大差異便是一個(gè)在首參數(shù)后傳入各個(gè)參數(shù),一個(gè)是在首參數(shù)后傳入一個(gè)包含所有參數(shù)的數(shù)組。如果我們?cè)趯?shí)現(xiàn)某些函數(shù)或方法時(shí),也希望實(shí)現(xiàn)像 call
一樣的使用方法,在 ES2015 之前,我們可能需要這樣做:
function fetchSomethings() {
var args = [].slice.apply(arguments)
// ...
}
function doSomeOthers(name) {
var args = [].slice.apply(arguments, 1)
// ...
}
而在 ES2015 中,我們可以很簡單的使用 …
語法糖來實(shí)現(xiàn):
function fetchSomethings(...args) {
// ...
}
function doSomeOthers(name, ...args) {
// ...
}
要注意的是,...args
后不可再添加
雖然從語言角度看,arguments
和 ...args
是可以同時(shí)使用 ,但有一個(gè)特殊情況則不可:arguments
在箭頭函數(shù)中,會(huì)跟隨上下文綁定到上層,所以在不確定上下文綁定結(jié)果的情況下,盡可能不要再箭頭函數(shù)中再使用 arguments
,而使用 ...args
。
雖然 ECMA 委員會(huì)和各類編譯器都無強(qiáng)制性要求用 ...args
代替 arguments
,但從實(shí)踐經(jīng)驗(yàn)看來,...args
確實(shí)可以在絕大部份場(chǎng)景下可以代替 arguments
使用,除非你有很特殊的場(chǎng)景需要使用到 arguments.callee
和 arguments.caller
。所以我推薦都使用 ...args
而非 arguments
。
PS:在嚴(yán)格模式(Strict Mode)中,arguments.callee
和 arguments.caller
是被禁止使用的。
解構(gòu)傳參
在 ES2015 中,...
語法還有另外一個(gè)功能:無上下文綁定的 apply
。什么意思?看看代碼你就知道了。
function sum(...args) {
return args.map(Number)
.reduce((a, b) => a + b)
}
console.log(sum(...[1, 2, 3])) //=> 6
有什么卵用?我也不知道(⊙o⊙)... Sorry...
注意事項(xiàng)
默認(rèn)參數(shù)值和后續(xù)參數(shù)需要遵循順序原則,否則會(huì)出錯(cuò)。
function(...args, last = 1) {
// This will go wrong
}
另外,根據(jù)函數(shù)調(diào)用的原則,無論是默認(rèn)參數(shù)值還是后續(xù)參數(shù)都需要小心使用。
新的數(shù)據(jù)結(jié)構(gòu)
在介紹新的數(shù)據(jù)結(jié)構(gòu)之前,我們先復(fù)習(xí)一下在 ES2015 之前,JavaScript 中有哪些基本的數(shù)據(jù)結(jié)構(gòu)。
- String 字符串
- Number 數(shù)字(包含整型和浮點(diǎn)型)
- Boolean 布爾值
- Object 對(duì)象
- Array 數(shù)組
其中又分為值類型和引用類型,Array 其實(shí)是 Object 的一種子類。
Set 和 WeakSet
我們?cè)賮韽?fù)習(xí)下高中數(shù)學(xué)吧,集不能包含相同的元素,我們可以根據(jù)元素畫出多個(gè)集的韋恩圖…………
好了跑題了。是的,在 ES2015 中,ECMA 委員會(huì)為 ECMAScript 增添了集(Set)和“弱”集(WeakSet)。它們都具有元素唯一性,若添加了已存在的元素,會(huì)被自動(dòng)忽略。
let s = new Set()
s.add('hello').add('world').add('hello')
console.log(s.size) //=> 2
console.log(s.has('hello')) //=> true
在實(shí)際開發(fā)中,我們有很多需要用到集的場(chǎng)景,如搜索、索引建立等。
咦?怎么還有一個(gè) WeakSet?這是干什么的?我曾經(jīng)寫過一篇關(guān)于 JavaScript 內(nèi)存優(yōu)化 的文章,而其中大部份都是在語言上動(dòng)手腳,而 WeakSet 則是在數(shù)據(jù)上做文章。
WeakSet 在 JavaScript 底層作出調(diào)整(在非降級(jí)兼容的情況下),檢查元素的變量引用情況。如果元素的引用已被全部解除,則該元素就會(huì)被刪除,以節(jié)省內(nèi)存空間。這意味著無法直接加入數(shù)字或者字符串。另外 WeakSet 對(duì)元素有嚴(yán)格要求,必須是 Object,當(dāng)然了,你也可以用 new String('...')
等形式處理元素。
let weaks = new WeakSet()
weaks.add("hello") //=> Error
weaks.add(3.1415) //=> Error
let foo = new String("bar")
let pi = new Number(3.1415)
weaks.add(foo)
weaks.add(pi)
weaks.has(foo) //=> true
foo = null
weaks.has(foo) //=> false
Map 和 WeakMap
從數(shù)據(jù)結(jié)構(gòu)的角度來說,映射(Map)跟原本的 Object 非常相似,都是 Key/Value 的鍵值對(duì)結(jié)構(gòu)。但是 Object 有一個(gè)讓人非常不爽的限制:key 必須是字符串或數(shù)字。在一般情況下,我們并不會(huì)遇上這一限制,但若我們需要建立一個(gè)對(duì)象映射表時(shí),這一限制顯得尤為棘手。
而 Map 則解決了這一問題,可以使用任何對(duì)象作為其 key,這可以實(shí)現(xiàn)從前不能實(shí)現(xiàn)或難以實(shí)現(xiàn)的功能,如在項(xiàng)目邏輯層實(shí)現(xiàn)數(shù)據(jù)索引等。
let map = new Map()
let object = { id: 1 }
map.set(object, 'hello')
map.set('hello', 'world')
map.has(object) //=> true
map.get(object) //=> hello
而 WeakMap 和 WeakSet 很類似,只不過 WeakMap 的鍵和值都會(huì)檢查變量引用,只要其一的引用全被解除,該鍵值對(duì)就會(huì)被刪除。
let weakm = new WeakMap()
let keyObject = { id: 1 }
let valObject = { score: 100 }
weakm.set(keyObject, valObject)
weakm.get(keyObject) //=> { score: 100 }
keyObject = null
weakm.has(keyObject) //=> false
類(Classes)
類,作為自 JavaScript 誕生以來最大的痛點(diǎn)之一,終于在 ES2015 中得到了官方的妥協(xié),“實(shí)現(xiàn)”了 ECMAScript 中的標(biāo)準(zhǔn)類機(jī)制。為什么是帶有雙引號(hào)的呢?因?yàn)槲覀儾浑y發(fā)現(xiàn)這樣一個(gè)現(xiàn)象:
$ node
> class Foo {}
[Function: Foo]
回想一下在 ES2015 以前的時(shí)代中,我們是怎么在 JavaScript 中實(shí)現(xiàn)類的?
function Foo() {}
var foo = new Foo()
是的,ES6 中的類只是一種語法糖,用于定義原型(Prototype)的。當(dāng)然,餓死的廚師三百斤,有總比沒有強(qiáng),我們還是很欣然地接受了這一設(shè)定。
語法
定義
與大多數(shù)人所期待的一樣,ES2015 所帶來的類語法確實(shí)與很多 C 語言家族的語法相似。
class Person {
constructor(name, gender, age) {
this.name = name
this.gender = gender
this.age = age
}
isAdult() {
return this.age >= 18
}
}
let me = new Person('iwillwen', 'man', 19)
console.log(me.isAdult()) //=> true
與 JavaScript 中的對(duì)象字面量不一樣的是,類的屬性后不能加逗號(hào),而對(duì)象字面量則必須要加逗號(hào)。
然而,讓人很不爽的是,ES2015 中對(duì)類的定義依然不支持默認(rèn)屬性的語法:
// 理想型
class Person {
name: String
gender = 'man'
// ...
}
而在 TypeScript 中則有良好的實(shí)現(xiàn)。
繼承
ES2015 的類繼承總算是為 JavaScript 類繼承之爭拋下了一根定海神針了。在此前,有各種 JavaScript 的繼承方法被發(fā)明和使用。(詳細(xì)請(qǐng)參見《JavaScript 高級(jí)程序設(shè)計(jì)》)
class Animal {
yell() {
console.log('yell')
}
}
class Person extends Animal {
constructor(name, gender, age) {
super() // must call `super` before using `this` if this class has a superclass
this.name = name
this.gender = gender
this.age = age
}
isAdult() {
return this.age >= 18
}
}
class Man extends Person {
constructor(name, age) {
super(name, 'man', age)
}
}
let me = new Man('iwillwen', 19)
console.log(me.isAdult()) //=> true
me.yell()
同樣的,繼承的語法跟許多語言中的很類似,ES2015 中若要是一個(gè)類繼承于另外一個(gè)類而作為其子類,只需要在子類的名字后面加上 extends {SuperClass}
即可。
靜態(tài)方法
ES2015 中的類機(jī)制支持 static
類型的方法定義,比如說 Man
是一個(gè)類,而我希望為其定義一個(gè) Man.isMan()
方法以用于類型檢查,我們可以這樣做:
class Man {
// ...
static isMan(obj) {
return obj instanceof Man
}
}
let me = new Man()
console.log(Man.isMan(me)) //=> true
遺憾的是,ES2015 的類并不能直接地定義靜態(tài)成員變量,但若必須實(shí)現(xiàn)此類需求,可以用static
加上 get
語句和 set
語句實(shí)現(xiàn)。
class SyncObject {
// ...
static get baseUrl() {
return 'http:///api/sync'
}
}
遺憾與期望
就目前來說,ES2015 的類機(jī)制依然很雞肋:
- 不支持私有屬性(
private
)
- 不支持前置屬性定義,但可用
get
語句和 set
語句實(shí)現(xiàn)
- 不支持多重繼承
- 沒有類似于協(xié)議(
Protocl
)或接口(Interface
)等的概念
中肯地說,ES2015 的類機(jī)制依然有待加強(qiáng)。但總的來說,是值得嘗試和討論的,我們可以像從前一樣,不斷嘗試新的方法,促進(jìn) ECMAScript 標(biāo)準(zhǔn)的發(fā)展。
生成器(Generator)
終于到了 ES2015 中我最喜歡的特性了,前方高能反應(yīng),所有人立刻進(jìn)入戰(zhàn)斗準(zhǔn)備!
為什么說這是我最喜歡的新特性呢?對(duì)于一個(gè)純前端的 JavaScript 工程師來說,可能 Generator 并沒有什么卵用,但若你曾使用過 Node.js 或者你的前端工程中有大量的異步操作,Generator 簡直是你的“賢者之石”。(不過,這并不是 Generator 最正統(tǒng)的用法。出于嚴(yán)謹(jǐn),我會(huì)從頭開始講述 Generator)
來龍
Generator 的設(shè)計(jì)初衷是為了提供一種能夠簡便地生成一系列對(duì)象的方法,如計(jì)算斐波那契數(shù)列(Fibonacci Sequence):
function* fibo() {
let a = 1
let b = 1
yield a
yield b
while (true) {
let next = a + b
a = b
b = next
yield next
}
}
let generator = fibo()
for (var i = 0; i < 10; i++)
console.log(generator.next().value) //=> 1 1 2 3 5 8 13 21 34 55
如果你沒有接觸過 Generator,你一定會(huì)對(duì)這段代碼感到很奇怪:為什么 function
后會(huì)有一個(gè) *
?為什么函數(shù)里使用了 while (true)
卻沒有進(jìn)入死循環(huán)而導(dǎo)致死機(jī)?yield
又是什么鬼?
不著急,我們一一道來。
基本概念
在學(xué)習(xí)如何使用 Generator 之前,我們先了解一些必要的概念。
Generator Function
生成器函數(shù)用于生成生成器(Generator),它與普通函數(shù)的定義方式的區(qū)別就在于它需要在 function
后加一個(gè) *
。
function* FunctionName() {
// ...Generator Body
}
生成器函數(shù)的聲明形式不是必須的,同樣可以使用匿名函數(shù)的形式。
let FunctionName = function*() { /* ... */ }
生成器函數(shù)的函數(shù)內(nèi)容將會(huì)是對(duì)應(yīng)生成器的運(yùn)行內(nèi)容,其中支持一種新的語法 yield
。它的作用與 return
有點(diǎn)相似,但并非退出函數(shù),而是切出生成器運(yùn)行時(shí)。
你可以把整個(gè)生成器運(yùn)行時(shí)看成一條長長的面條(while (true)
則就是無限長的),JavaScript 引擎在每一次遇到 yield
就要切一刀,而切面所成的“紋路”則是 yield
出來的值。

~~好吧這是瑞士卷~~
Generator
生(rui)成(shi)器(juan)在某種意義上可以看做為與 JavaScript 主線程分離的運(yùn)行時(shí)(詳細(xì)可參考我的另外一篇文章:http:///koa-co-and-coroutine/),它可以隨時(shí)被 yield
切回主線程(生成器不影響主線程)。
每一次生成器運(yùn)行時(shí)被 yield
都可以帶出一個(gè)值,使其回到主線程中;此后,也可以從主線程返回一個(gè)值回到生成器運(yùn)行時(shí)中:
let inputValue = yield outputValue
生成器切出主線程并帶出 outputValue
,主函數(shù)經(jīng)過處理后(可以是異步的),把 inputValue
帶回生成器中;主線程可以通過 .next(inputValue)
方法返回值到生成器運(yùn)行時(shí)中。
基本使用方法
構(gòu)建生成器函數(shù)
使用 Generator 的第一步自然是要構(gòu)建生成器函數(shù),理清構(gòu)建思路,比如我需要做一個(gè)生成斐波那契數(shù)列(俗稱兔子數(shù)列)的生成器們則需要如何構(gòu)建循環(huán)體呢?如果我需要在主線程不斷獲得結(jié)果,則需要在生成器 中做無限循環(huán),以保證其不斷地生成。
而根據(jù)斐波那契數(shù)列的定義,第 n (n ≥ 3) 項(xiàng)是第 n - 1 項(xiàng)和第 n - 2 之和,而第 1 項(xiàng)和第 2 項(xiàng)都是 1。
function* fibo() {
let [a, b] = [1, 1]
yield a
yield b
while (true) {
[a, b] = [b, a + b]
yield b
}
}
這樣設(shè)計(jì)生成器函數(shù),就可以先把預(yù)先設(shè)定好的首兩項(xiàng)輸出,然后通過無限循環(huán)不斷把后一項(xiàng)輸出。
啟動(dòng)生成器
生成器函數(shù)不能直接用來作為生成器使用,需要先使用這個(gè)函數(shù)得到一個(gè)生成器,用于運(yùn)行生成器內(nèi)容和接收返回值。
let gen = fibo()
運(yùn)行生成器內(nèi)容
得到生成器以后,我們就可以通過它進(jìn)行數(shù)列項(xiàng)生成了。此處演示獲得前 10 項(xiàng)。
let arr = []
for (let i = 0; i < 10; i++)
arr.push(gen.next().value)
console.log(arr) //=> [ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ]
你也可以通過圖示理解 Generator 的運(yùn)行原理

事實(shí)上,Generator 的用法還是很多種,其中最為著名的一種便是使用 Generator 的特性模擬出 ES7 中的 async/await 特性。而其中最為著名的就是 co 和 koa(基于 co 的 Web Framework) 了。詳細(xì)可以看我的另外一篇文章:Koa, co and coroutine。
原生的模塊化
在前文中,我提到了 ES2015 在工程化方面上有著良好的優(yōu)勢(shì),而采用的就是 ES2015 中的原生模塊化機(jī)制,足以證明它的重要性。
歷史小回顧
在 JavaScript 的發(fā)展歷史上,曾出現(xiàn)過多種模塊加載庫,如 RequireJS、SeaJS、FIS 等,而由它們衍生出來的 JavaScript 模塊化標(biāo)準(zhǔn)有 CommonJS、AMD、CMD 和 UMD 等。
其中最為典型的是 Node.js 所遵循的 CommonJS 和 RequireJS 的 AMD。
本文在此不再詳細(xì)說明這些模塊化方案,詳細(xì)可以閱讀 What Is AMD, CommonJS, and UMD?
基本用法
正如前文所展示的使用方式一樣,ES2015 中的模塊化機(jī)制設(shè)計(jì)也是相當(dāng)成熟的?;旧纤械?CommonJS 或是 AMD 代碼都可以很快地轉(zhuǎn)換為 ES2015 標(biāo)準(zhǔn)的加載器代碼。
import name from "module-name"
import * as name from "module-name"
import { member } from "module-name"
import { member as alias } from "module-name"
import { member1 , member2 } from "module-name"
import { member1 , member2 as alias2 , [...] } from "module-name"
import defaultMember, { member [ , [...] ] } from "module-name"
import defaultMember, * as alias from "module-name"
import defaultMember from "module-name"
import "module-name"
// Copy from Mozilla Developer Center
如上所示,ES2015 中有很多種模塊引入方式,我們可以根據(jù)實(shí)際需要選擇一種使用。
全局引入
全局引入是最基本的引入方式,這跟 CommonJS、AMD 等模塊化標(biāo)準(zhǔn)并無兩樣,都是把目標(biāo)模塊的所有暴露的接口引入到一個(gè)命名空間中。
import name from 'module-name'
import * as name from 'module-name'
這跟 Node.js 所用的 CommonJS 類似:
var name = require('module-name')
局部引入
與 CommonJS 等標(biāo)準(zhǔn)不同的是,ES2015 的模塊引入機(jī)制支持引入模塊的部份暴露接口,這在大型的組件開發(fā)中顯得尤為方便,如 React 的組件引入便是使用了該特性。
import { A, B, C } from 'module-name'
A()
B()
C()
接口暴露
ES2015 的接口暴露方式比 CommonJS 等標(biāo)準(zhǔn)都要豐富和健壯,可見 ECMA 委員會(huì)對(duì)這一部份的重視程度之高。
ES2015 的接口暴露有幾種用法:
暴露單獨(dú)接口
// module.js
export function method() { /* ... */ }
// main.js
import M from './module'
M.method()
基本的 export
語句可以調(diào)用多次,單獨(dú)使用可暴露一個(gè)對(duì)象到該模塊外。
暴露復(fù)蓋模塊
若需要實(shí)現(xiàn)像 CommonJS 中的 module.exports = {}
以覆蓋整個(gè)模塊的暴露對(duì)象,則需要在 export
語句后加上 default
。
// module.js
export default {
method1,
method2
}
// main.js
import M from './module'
M.method1()
降級(jí)兼容
在實(shí)際應(yīng)用中,我們暫時(shí)還需要使用 babel 等工具對(duì)代碼進(jìn)行降級(jí)兼容。慶幸的是,babel 支持 CommonJS、AMD、UMD 等模塊化標(biāo)準(zhǔn)的降級(jí)兼容,我們可以根據(jù)項(xiàng)目的實(shí)際情況選擇降級(jí)目標(biāo)。
$ babel -m common -d dist/common/ src/
$ babel -m amd -d dist/amd/ src/
$ babel -m umd -d dist/umd/ src/
Promise
Promise,作為一個(gè)老生常談的話題,早已被聰明的工程師們“玩壞”了。
光是 Promise 自身,目前就有多種標(biāo)準(zhǔn),而目前最為流行的是 Promises/A+。而 ES2015 中的 Promise 便是基于 Promises/A+ 制定的。
概念
Promise 是一種用于解決回調(diào)函數(shù)無限嵌套的工具(當(dāng)然,這只是其中一種),其字面意義為“保證”。它的作用便是“免去”異步操作的回調(diào)函數(shù),保證能通過后續(xù)監(jiān)聽而得到返回值,或?qū)﹀e(cuò)誤處理。它能使異步操作變得井然有序,也更好控制。我們以在瀏覽器中訪問一個(gè) API,解析返回的 JSON 數(shù)據(jù)。
fetch('http:///api/users/top')
.then(res => res.json())
.then(data => {
vm.data.topUsers = data
})
// Handle the error crash in the chaining processes
.catch(err => console.error(err))
Promise 在設(shè)計(jì)上具有原子性,即只有兩種狀態(tài):未開始和結(jié)束(無論成功與否都算是結(jié)束),這讓我們?cè)谡{(diào)用支持 Promise 的異步方法時(shí),邏輯將變得非常簡單,這在大規(guī)模的軟件工程開發(fā)中具有良好的健壯性。
基本用法
創(chuàng)建 Promise 對(duì)象
要為一個(gè)函數(shù)賦予 Promise 的能力,先要?jiǎng)?chuàng)建一個(gè) Promise 對(duì)象,并將其作為函數(shù)值返回。Promise 構(gòu)造函數(shù)要求傳入一個(gè)函數(shù),并帶有 resolve
和 reject
參數(shù)。這是兩個(gè)用于結(jié)束 Promise 等待的函數(shù),對(duì)應(yīng)的成功和失敗。而我們的邏輯代碼就在這個(gè)函數(shù)中進(jìn)行。
此處,因?yàn)楸仨氁屵@個(gè)函數(shù)包裹邏輯代碼,所以如果需要用到 this
時(shí),則需要使用箭頭函數(shù)或者在前面做一個(gè) this
的別名。
function fetchData() {
return new Promise((resolve, reject) => {
// ...
})
}
進(jìn)行異步操作
事實(shí)上,在異步操作內(nèi),并不需要對(duì) Promise 對(duì)象進(jìn)行操作(除非有特殊需求)。
function fetchData() {
return new Promise((resolve, reject) => {
api.call('fetch_data', (err, data) => {
if (err) return reject(err)
resolve(data)
})
})
}
因?yàn)樵?Promise 定義的過程中,也會(huì)出現(xiàn)數(shù)層回調(diào)嵌套的情況,如果需要使用 this
的話,便顯現(xiàn)出了箭頭函數(shù)的優(yōu)勢(shì)了。
使用 Promise
讓異步操作函數(shù)支持 Promise 后,我們就可以享受 Promise 帶來的優(yōu)雅和便捷了~
fetchData()
.then(data => {
// ...
return storeInFileSystem(data)
})
.then(data => {
return renderUIAnimated(data)
})
.catch(err => console.error(err))
弊端
雖說 Promise 確實(shí)很優(yōu)雅,但是這是在所有需要用到的異步方法都支持 Promise 且遵循標(biāo)準(zhǔn)。而且鏈?zhǔn)?Promise 強(qiáng)制性要求邏輯必須是線性單向的,一旦出現(xiàn)如并行、回溯等情況,Promise 便顯得十分累贅。
所以在目前的最佳實(shí)踐中,Promise 會(huì)作為一種接口定義方法,而不是邏輯處理工具。后文將會(huì)詳細(xì)闡述這種最佳實(shí)踐。
Symbol
Symbol 是一種很有意思的概念,它跟 Swift 中的 Selector 有點(diǎn)相像,但也更特別。在 JavaScript 中,對(duì)象的屬性名稱可以是字符串或數(shù)字。而如今又多了一個(gè) Symbol。那 Symbol 究竟有什么用?
首先,我們要了解的是,Symbol 對(duì)象是具有唯一性的,也就是說,每一個(gè) Symbol 對(duì)象都是唯一的,即便我們看不到它的區(qū)別在哪里。這就意味著,我們可以用它來保證一些數(shù)據(jù)的安全性。
console.log(Symbol('key') == Symbol('key')) //=> false
如果將一個(gè) Symbol 隱藏于一個(gè)封閉的作用域內(nèi),并作為一個(gè)對(duì)象中某屬性的鍵,則外層作用域中便無法取得該屬性的值,有效保障了某些私有庫的代碼安全性。
let privateDataStore = {
set(val) {
let key = Symbol(Math.random().toString(32).substr(2))
this[key] = val
return key
},
get(key) {
return this[key]
}
}
let key = privateDateStore('hello world')
privateDataStore[key] //=> undefined
privateDataStore.get(key) //=> hello world
如果你想通過某些辦法取得被隱藏的 key 的話,我只能說:理論上,不可能。
let obj = {}
let key = Symbol('key')
obj[key] = 1
JSON.stringify(obj) //=> {}
Object.keys(obj) //=> []
obj[key] //=> 1
黑科技
Symbol 除了帶給我們數(shù)據(jù)安全性以外,還帶來了一些很神奇的黑科技,簡直了。
Symbol.iterator
除 Symbol 以外,ES2015 還為 JavaScript 帶來了 for...of
語句,這個(gè)跟原本的 for...in
又有什么區(qū)別?
我們還是以前面的斐波那契數(shù)列作為例子。Iterator 在 Java 中經(jīng)常用到中會(huì)經(jīng)常用到,意為“迭代器”,你可以把它理解為用于循環(huán)的工具。
let fibo = {
[ Symbol.iterator ]() {
let a = 0
let b = 1
return {
next() {
[a, b] = [b, a + b]
return { done: false, value: b }
}
}
}
}
for (let n of fibo) {
if (n > 100) break
console.log(n)
}
Wow! 看到這個(gè) for…of
是否有種興奮的感覺?雖然說創(chuàng)建 fibo
的時(shí)候稍微有點(diǎn)麻煩……
不如我們先來看看這個(gè) fibo
究竟是怎么定義出來了。首先,我們要了解到 JavaScript 引擎(或編譯器)在處理 for...of
的時(shí)候,會(huì)從 of
后的對(duì)象取得 Symbol.iterator
這屬性鍵的值,為一個(gè)函數(shù)。它要求要返回一個(gè)包含 next
方法的對(duì)象,用于不斷迭代。而因?yàn)?Symbol.iterator
所在鍵值對(duì)的值是一個(gè)函數(shù),這就讓我們有了自由發(fā)揮的空間,比如定義局部變量等等。
每當(dāng) for...of
進(jìn)行了一次循環(huán),都會(huì)執(zhí)行一次該對(duì)象的 next
方法,已得到下一個(gè)值,并檢查是否迭代完成。隨著 ES7 的開發(fā),for...of
所能發(fā)揮的潛能將會(huì)越來越強(qiáng)。
還有更多的 Symbol 黑科技等待挖掘,再次本文不作詳細(xì)闡述,如有興趣,可以看看 Mozilla Developer Center 上的介紹。
Proxy(代理)
Proxy 是 ECMAScript 中的一種新概念,它有很多好玩的用途,從基本的作用說就是:Proxy 可以在不入侵目標(biāo)對(duì)象的情況下,對(duì)邏輯行為進(jìn)行攔截和處理。
比如說我想記錄下我代碼中某些接口的使用情況,以供數(shù)據(jù)分析所用,但是因?yàn)槟繕?biāo)代碼中是嚴(yán)格控制的,所以不能對(duì)其進(jìn)行修改,而另外寫一個(gè)對(duì)象來對(duì)目標(biāo)對(duì)象做代理也很麻煩。那么 Proxy 便可以提供一種比較簡單的方法來實(shí)現(xiàn)這一需求。
假設(shè)我要對(duì) api
這一對(duì)象進(jìn)行攔截并記錄下代碼行為,我就可以這樣做:
let apiProxy = new Proxy(api, {
get(receiver, name) {
return (function(...args) {
min.sadd(`log:${name}`, args)
return receiver[name].apply(receiver, args)
}).bind(receiver)
}
})
api.getComments(artical.id)
.then(comments => {
// ...
})
可惜的是,目前 Proxy 的兼容性很差,哪怕是降級(jí)兼容也難以實(shí)現(xiàn)。
到這里,相信你已經(jīng)對(duì) ES2015 中的大部份新特性有所了解了。那么現(xiàn)在,就結(jié)合我們?cè)械?JavaScript 技能,開始使用 ES2015 構(gòu)建一個(gè)具有工程化特點(diǎn)的項(xiàng)目吧。
ES2015 的前端開發(fā)實(shí)戰(zhàn)
事實(shí)上,你們都應(yīng)該有聽說過 React 這個(gè)來自 Facebook 的前端框架,因?yàn)楝F(xiàn)在它實(shí)在太火了。React 與 ES2015 的關(guān)系可謂深厚,React 在開發(fā)上便要求使用 ES2015 標(biāo)準(zhǔn),因其 DSL ── JSX 的存在,所以必須要依賴 Babel 將其編譯成 JavaScript。
但同樣是由于 JSX 的存在,本文章并不會(huì)采用 React 作為前端框架,以避免讀者對(duì) JSX 和 HTML 的誤解。我們會(huì)采用同樣優(yōu)秀的前端 MVVM 框架 ── Vue 進(jìn)行開發(fā)。
數(shù)據(jù)部份,將會(huì)使用 MinDB 進(jìn)行存儲(chǔ)和處理。MinDB 是由筆者開發(fā)的一個(gè)用于 JavaScript 環(huán)境的簡易而健壯的數(shù)據(jù)庫,它默認(rèn)使用 DOM Storage 作為其存儲(chǔ)容器,在其他環(huán)境中可以通過更換 Store Interface 以兼容絕大部份 JavaScript 運(yùn)行環(huán)境。
Vue.js 的使用教程可以參考 Vue.js 的官方教程。
構(gòu)建界面
我們首先簡單地用 LayoutIt 搭建一個(gè)用 Bootstrap 構(gòu)架的頁面,其中包含了 DEMO 的首頁和文章內(nèi)容頁,此后我們將會(huì)使用這個(gè)模板搭建我們的 JavaScript 代碼架構(gòu)。


接下來,我們需要通過對(duì)頁面的功能塊進(jìn)行組件劃分,以便于使用組件化的架構(gòu)搭建前端頁面。
我們可以大致分為 Index、Post 和 Publish 三個(gè)頁面,也可以說三個(gè)路由方向;而我們還可以把頁面中的組塊分為:文章列表、文章、側(cè)邊欄、評(píng)論框等。
以此,我們可以設(shè)計(jì)出以下結(jié)構(gòu),以作為這個(gè)項(xiàng)目的組織結(jié)構(gòu):
Routes Components
|- Index ----|- Posts
| |- Sidebar
|
|- Post -----|- Post
| |- Comments
|
|- Publish
首頁包含了文章列表、側(cè)邊欄兩個(gè)組件;文章頁面包含文章內(nèi)容組件和評(píng)論框組件(此處我們使用多說評(píng)論框作為我們的組件);而文章發(fā)布頁則可以單獨(dú)為一個(gè)路由器,而不需要分出組件。
代碼結(jié)構(gòu)定義
因我們是以 babel 進(jìn)行 ES2015 降級(jí)兼容的,所以我們最好可以采用分離的結(jié)構(gòu),這里我們使用 src
和 dist
。
我們此處以比較簡單的結(jié)構(gòu)構(gòu)建我們的DEMO:
app
|- src 程序的源文件目錄
| |- controllers 后端的路由處理器
| |- lib 后端需要引用的一些庫
| |- public 前端 JavaScript 源文件
| | |- controllers 前端的路由處理器
| | |- components 前端組件
| | |- models 前端數(shù)據(jù)層
| | |- config.js 前端的配置文件
| | |- main.js 前端 JavaScript 入口
| |- app.js 后端程序入口
| |- routes.js 后端路由表
|- dist 降級(jí)兼容輸出目錄
| |- public
| |- css
| |- index.html 前端 HTML 入口
|- gulpfile.js Gulp 構(gòu)建配置文件
|- package.json Node.js 項(xiàng)目配置文件
而我們?cè)谶@一章節(jié)中則專注于 public
這一目錄即可,Node.js 部份將在下一章節(jié)詳細(xì)展示。
架構(gòu)設(shè)計(jì)
模塊化
因?yàn)橛辛?ES2015 自身的模塊化機(jī)制,我們就不必使用 RequireJS 等模塊加載庫了,通過 Browserify 我們我可以將整個(gè)前端的 JavaScript 程序打包到一個(gè) .js 文件中,而這一步驟我們使用 Gulp 來完成。
詳細(xì)的 Gulp 使用教程可以參考:了不起的任務(wù)運(yùn)行器Gulp基礎(chǔ)教程
數(shù)據(jù)支持
我們的數(shù)據(jù)將從后端的 Node.js 程序中獲取,以 API 的形式獲得。
另外,為了得到更佳的用戶體驗(yàn),我們將使用 MinDB 作為這個(gè) DEMO 的前端數(shù)據(jù)庫,減少網(wǎng)絡(luò)請(qǐng)求次數(shù),優(yōu)化使用體驗(yàn)。
界面渲染
為了能讓我們的界面定義能夠足夠簡單,我們?cè)谶@使用了 Vue 作為前端開發(fā)框架,將其與 MinDB 對(duì)接,負(fù)責(zé)渲染我們的頁面。
其中,我們會(huì)利用 Vue 的組件系統(tǒng)來實(shí)現(xiàn)我們制定下的組件設(shè)計(jì)。另外,我們還會(huì)使用 watchman.js 來實(shí)現(xiàn)前端的路由器,以便我們對(duì)不同的頁面的邏輯進(jìn)行分離。
構(gòu)建應(yīng)用
在開始編寫業(yè)務(wù)代碼之前,我們需要先安裝好我們所需要的依賴庫。
$ npm install vue min watchman-router marked --save
安裝好依賴庫以后,我們就開始編寫入口文件吧!
入口文件
// main.js
import Vue from 'vue'
import watch from 'watchman-router'
import qs from 'querystring' // From Node.js
watch({
// TODO
})
.use((ctx, next) => {
ctx.query = qs.parse(window.location.search.substr(1))
next()
})
.run()
在入口中,我們將做如下任務(wù):
- 引入所有的路由相應(yīng)器
- 將路由相應(yīng)器通過 watchman.js 綁定到對(duì)應(yīng)的 url 規(guī)則中
- 建立在共用模板中存在的需要實(shí)例化的組件
因?yàn)槲覀冞€么有開始動(dòng)工路由相應(yīng)器,所以我們先把“建立在共用模板中存在的需要實(shí)例化的組件”這一任務(wù)完成。
在共用模板中,有唯一一個(gè)必須的共用組件就是頁面切換器 ── 一個(gè)用于包含所有的頁面的元素。
<div class="row" id="wrapper" v-html="html"></div>
對(duì)應(yīng)的,我們將在入口文件中使用 Vue 建立其對(duì)應(yīng)的 ViewModel,并將其綁定至 DOM 元素。
let layoutVM = new Vue({
el: '#wrapper',
data: {
html: ''
}
})
以后可以通過改變 layoutVM.$data.html
來改變 #wrapper
的內(nèi)容,配合 watchman.js 以加載不同的頁面內(nèi)容。
為了能在路由相應(yīng)器中改變 layoutVM
的參數(shù),我們將其作為 watchman.js 給予相應(yīng)器的上下文參數(shù)中的一個(gè)屬性。
// ...
.use((ctx, next) => {
ctx.query = qs.parse(window.location.search.substr(1))
ctx.layoutVM = layoutVM
next()
})
數(shù)據(jù)層:文章
我們單獨(dú)把文章的查詢、讀取和創(chuàng)建等操作抽象成一個(gè)庫,使其與邏輯層分離,讓代碼更美觀。
在此之前,我們先定義好從后端用于取得文章數(shù)據(jù)的 API:
- URL:
/api/posts/list
- 參數(shù):
page
當(dāng)前頁數(shù),每頁 10 條記錄
我們可以直接用新的 Ajax API 來進(jìn)行 API 訪問。
import 'whatwg-fetch'
import min from 'min'
async function listPosts(page = 0) {
const count = 10
// 檢查 MinDB 是否存在數(shù)據(jù)
let existsInMinDB = await min.exists('posts:id')
if (!existsInMinDB) {
var posts = (await _fetchPost(page))
.map(post => {
return {
id: post._id,
title: post.title,
content: post.content,
author: post.author,
comments: post.comments.length,
get summary() {
return post.content.substr(0, 20) + '...'
}
}
})
// 將數(shù)據(jù)存儲(chǔ)到 MinDB 中
for (let i = 0; i < posts.length; i++) {
let post = posts[i]
await min.sadd('posts:id', post.id)
await min.hmset(`post:${post.id}`, post)
}
} else {
// 從 MinDB 讀取數(shù)據(jù)
let ids = await min.smembers('posts:id')
ids = ids.slice(page * count, (page + 1) * count)
var posts = await min.mget(ids.map(id => `post:${id}`))
}
return posts
}
async function _fetchPost(page) {
// 通過 `fetch` 訪問 API
let res = await fetch(`/api/posts/list?page=${page}`)
let reply = await res.json()
return reply.posts
}
其中,min.sadd('posts:id', post.id)
會(huì)將文章的 ID 存入 MinDB 中名為 posts:id
的集中,這個(gè)集用于保存所有文章的 ID;min.hmset(`post:${post.id}`, post)
則會(huì)將文章的所有數(shù)據(jù)存入以 post:{id}
命名的 Hash 中。
完成對(duì)首頁的所有文章列表支持后,我們還需要簡單的定義一個(gè)用于讀取單篇文章數(shù)據(jù)的 API。
如果用戶是從首頁的文章列表進(jìn)來的,那么我們就可以直接從 MinDB 中讀取文章數(shù)據(jù)。但如果用戶是直接通過 url 打開網(wǎng)址的話,MinDB 有可能并沒有存儲(chǔ)文章數(shù)據(jù),那么我們就通過 API 從后端獲取數(shù)據(jù),并存儲(chǔ)在 MinDB 中。
async function getPost(id) {
let existsInMinDB = await min.exists(`post:${id}`)
if (existsInMinDB) {
return await min.hgetall(`post:${id}`)
} else {
let res = await fetch(`/api/posts/${id}`)
let post = (await res.json()).post
await min.hmset(`post:${id}`, {
id: post._id,
title: post.title,
content: post.content,
author: post.author,
comments: post.comments.length,
get summary() {
return post.content.substr(0, 20) + '...'
}
})
return post
}
}
完成用于讀取的接口后,我們也該做做用于寫入的接口了。同樣的,我們也先來把 API 定義一下。
- URL:
/api/posts/new
- Body(JSON):
我們也可以通過 fetch
來發(fā)出 POST 請(qǐng)求。
async function publishPost(post) {
let res = await fetch('/api/posts/new', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(post)
})
var _post = await res.json()
await min.sadd('posts:id', _post._id)
await min.hmset(`post:${_post._id}`, {
id: _post._id,
title: _post.title,
content: _post.content,
author: _post.author,
comments: 0,
get summary() {
return _post.title.substr(0, 20) + '...'
}
})
_post.id = _post._id
return _post
}
最后我們就可以暴露出這幾個(gè)接口了。
export default {
listPosts,
getPost,
publishPost
}
路由:首頁
首先我們確定一下首頁中我們需要做些什么:
- 改變
layoutVM
的 HTML 數(shù)據(jù),為后面的頁面渲染做準(zhǔn)備
- 分別從后端加載文章數(shù)據(jù)和多說加載評(píng)論數(shù),加載完以后存入 MinDB 中
- 從 MinDB 中加載已緩存的數(shù)據(jù)
- 建立對(duì)應(yīng)的 VM,并傳入數(shù)據(jù)
準(zhǔn)備頁面渲染
首先,我們需要為 watchman.js 的路由提供一個(gè)函數(shù)以作相應(yīng)器,并包含一個(gè)為 context
的參數(shù)。我們則可以將其作為模塊的暴露值。
export default function(ctx) {
// 改變 layoutVM
ctx.layoutVM.$data.html = `
<h1>
ES2015 實(shí)戰(zhàn) - DEMO
</h1>
<hr>
<div id="posts" class="col-md-9">
<post-in-list v-repeat="posts"></post-in-list>
</div>
<div id="sidebar" class="col-md-3">
<panel title="側(cè)邊欄" content="{{content}}" list="{{list}}"></panel>
</div>
`
}
此處將首頁的 HTML 結(jié)構(gòu)賦予 layoutVM
,并讓其渲染至頁面中。
加載數(shù)據(jù)
因?yàn)槲覀冎耙呀?jīng)將數(shù)據(jù)層抽象化了,所以我們此處只需通過我們的抽象數(shù)據(jù)層讀取我們所需要的數(shù)據(jù)即可。
import Vue from 'vue'
import Posts from '../models/posts'
// ...
export default async function(ctx) {
let refresh = 'undefined' != typeof ctx.query.refresh
let page = ctx.query.page || 0
let posts = await Posts.listPosts(page)
}
設(shè)計(jì)組件
在首頁的 HTML 中,我們用到了兩個(gè) Component,分別為 post-in-list
和 panel
,我們分別在 components
文件夾中分別建立 post-in-list.js
和 panel.js
。我們從我們之前通過 LayoutIt 簡單建立的 HTML 模板中,抽出對(duì)應(yīng)的部份,并將其作為 Vue Component 的模板。
// post-in-list.js
import Vue from 'vue'
import marked from 'marked'
// 模板
const template = `
<div class="post" v-attr="id: id">
<h2><a href="/#!/post/{{id}}" v-text="title"></a></h2>
<p v-text="summary"></p>
<p>
<small>由 {{author}} 發(fā)表</small> | <a class="btn" href="/#!/post/{{id}}">查看更多 ?</a>
</p>
</div>
`
我們可以通過 Vue 的雙向綁定機(jī)制將數(shù)據(jù)插入到模板中。
Vue.component('post-in-list', {
template: template,
replace: true
})
根據(jù) Vue 的組件機(jī)制,我們可以通過對(duì)組件標(biāo)簽中加入自定義屬性來傳入?yún)?shù),以供組件中使用。
但此處我們先用 Vue 的 v-repeat
指令來進(jìn)行循環(huán)使用組件。
<post-in-list v-repeat="posts"></post-in-list>
panel.js
同理建立。
路由:文章頁面
相比首頁,文章頁面要簡單得多。因?yàn)槲覀冊(cè)谑醉撘呀?jīng)將數(shù)據(jù)加載到 MinDB 中了,所以我們可以直接從 MinDB 中讀取數(shù)據(jù),然后將其渲染到頁面中。
// ...
let post = await min.hgetall(`post:${this.id}`)
// ...
然后,我們?cè)購闹霸O(shè)計(jì)好的頁面模板中,抽出我們需要用來作為文章頁面的內(nèi)容頁。
<h1 v-text="title"></h1>
<small>由 {{author}} 發(fā)表</small>
<div class="post" v-html="content | marked"></div>
我們同樣是同樣通過對(duì) layoutVM
的操作,來準(zhǔn)備頁面的渲染。在完成渲染準(zhǔn)備后,我們就可以開始獲取數(shù)據(jù)了。
let post = await min.hgetall(`post:${this.id}`)
在獲得相應(yīng)的文章數(shù)據(jù)以后,我們就可以通過建立一個(gè)組件來將其渲染至頁面中。其中,要注意的是我們需要通過 Vue 的一些 API 來整合數(shù)據(jù)、渲染等步驟。
在這我不再詳細(xì)說明其構(gòu)建步驟,與上一小節(jié)相同。
import Vue from 'vue'
import min from 'min'
import marked from 'marked'
const template = `
<h1 v-text="title"></h1>
<small>由 {{author}} 發(fā)表</small>
<div class="post" v-html="content | marked"></div>
`
let postVm = Vue.component('post', {
template: template,
replace: true,
props: [ 'id' ],
data() {
return {
id: this.id,
content: '',
title: ''
}
},
async created() {
this.$data = await min.hgetall(`post:${this.id}`)
},
filters: {
marked
}
})
export default postVm
此處我們除了 ES2015 的特性外,我們還更超前地使用了正在制定中的 ES7 的特性,比如 async/await
,這是一種用于對(duì)異步操作進(jìn)行“打扁”的特性,它可以把異步操作以同步的語法編寫。如上文所說,在 ES2015 中,我們可以用 co 來模擬 async/await
特性。
路由:發(fā)布新文章
在發(fā)布新文章的頁面中,我們直接調(diào)用我們之前建立好的數(shù)據(jù)抽象層的接口,將新數(shù)據(jù)傳向后端,并保存在 MinDB 中。
import marked from 'marked'
import Posts from '../models/posts'
// ...
new Vue({
el: '#new-post',
data: {
title: '',
content: '',
author: ''
},
methods: {
async submit(e) {
e.preventDefault()
var post = await Posts.publishPost({
title: this.$data.title,
content: this.$data.content,
author: this.$data.author
})
window.location.hash = `#!/post/${post.id}`
}
},
filters: {
marked
}
})
路由綁定
在完成路由響應(yīng)器的開發(fā)后,我們就可以把他們都綁定到 watchman.js 上了。
// ...
import Index from './controllers/index'
import Post from './controllers/post'
import Publish from './controllers/publish'
watch({
'/': Index,
'#!/': Index,
'#!/post/:id': Post,
'#!/new': Publish
})
// ...
這樣,就可以讓我們之前的路由結(jié)構(gòu)都綁定到入口文件中:
- 首頁綁定到
/
和 #!/
- 文章頁面則綁定到了
#!/post/:id
,比如 #!/post/123
則表示 id 為 123 的文章頁面
#!/new
則綁定了新建文章的頁面
合并代碼
在完成三個(gè)我們所構(gòu)建的路由設(shè)計(jì)后,我們就可以用 Browserify 把我們的代碼打包到一個(gè)文件中,以作為整個(gè)項(xiàng)目的入口文件。此處,我們?cè)僖?Gulp 作為我們的構(gòu)建輔助器,而不需要直接使用 Browserify 的命令行進(jìn)行構(gòu)建。
在開始編寫 Gulpfile 之前,我們先安裝我們所需要的依賴庫:
$ npm install gulp browserify babelify vinyl-source-stream vinyl-buffer babel-preset-es2015-without-regenerator babel-plugin-transform-async-to-generator --save
將依賴庫安裝好以后,我們就可以開始編寫 Gulp 的配置文件了。
在本文將近完成的時(shí)候,Babel 發(fā)布了版本 6,其 API 與 5 版本有著相當(dāng)大的區(qū)別,且 Babel 6 并不向前兼容,詳細(xì)更改此處不作介紹。
var gulp = require('gulp')
var browserify = require('browserify')
var babelify = require('babelify')
var source = require('vinyl-source-stream')
var buffer = require('vinyl-buffer')
gulp.task('browserify', function() {
return browserify({
entries: ['./src/public/main.js']
})
.transform(babelify.configure({
presets: [ 'es2015-without-regenerator' ],
plugins: [ 'transform-async-to-generator' ]
}))
.bundle()
.pipe(source('bundle.js'))
.pipe(buffer())
.pipe(gulp.dest('dist/public'))
})
gulp.task('default', [ 'browserify' ])
在這個(gè)配置文件中,我們把前端的代碼中的入口文件傳入 babel 中,然后將其打包成 bundle.js。
最后,我們可以在我們最開始通過 Layoutit 所設(shè)計(jì)的頁面中,把可以用于包含替換內(nèi)容的部份去掉,然后引入我們通過 Browserify 生成的 bundle.js。
完成 JavaScript 部份的開發(fā)后,我們?cè)賹⑺枰撵o態(tài)資源文件加載到 HTML 中,我們就可以看到這個(gè)基本脫離后端的前端 Web App 的效果了。

ES2015 的 Node.js 開發(fā)實(shí)戰(zhàn)
就目前來說,能最痛快地使用 ES2015 中各種新特性進(jìn)行 JavaScript 開發(fā)的環(huán)境,無疑就是 Node.js。就 Node.js 本身來說,就跟前端的 JavaScript 環(huán)境有著本質(zhì)上的區(qū)別,Node.js 有著完整意義上的異步 IO 機(jī)制, 有著無窮無盡的應(yīng)用領(lǐng)域,而且在語法角度上遇到問題機(jī)率比在前端大不少。甚至可以說,Node.js 一直是等著 ES2015 的到來的,Node.js 加上 ES2015 簡直就是如虎添翼了。
從 V8 引擎開始實(shí)驗(yàn)性的開始兼容 ES6 代碼時(shí),Node.js 便開始馬上跟進(jìn),在 Node.js 中開放 ES6 的兼容選項(xiàng),如 Generator、Classes 等等。經(jīng)過相當(dāng)長一段時(shí)間的測(cè)試和磨合后,就以在 Node.js 上使用 ES6 標(biāo)準(zhǔn)進(jìn)行應(yīng)用開發(fā)這件事來說,已經(jīng)變得越來越成熟,越來越多的開發(fā)者走上了 ES6 這條“不歸路”。
一些針對(duì) Node.js + ES6 的開發(fā)模式和第三方庫也如雨后春筍般冒出,其中最為人所熟知的便是以 co 為基礎(chǔ)所建立的 Web 框架 Koa。Koa 由 TJ 等 express 原班人馬打造,目前也有越來越多的中國開發(fā)者加入到 Koa 的開發(fā)團(tuán)隊(duì)中來,為前沿 Node.js 的開發(fā)做貢獻(xiàn)。
co 通過使用 ES2015 中的 Generator 特性來模擬 ES7 中相當(dāng)誘人的 async/await
特性,可以讓復(fù)雜的異步方法調(diào)用及處理變得像同步操作一樣簡單。引用響馬大叔在他所維護(hù)的某項(xiàng)目中的一句話:
用同步代碼抒發(fā)異步情懷
在本章節(jié)中,我將以一個(gè)簡單的后端架構(gòu)體系,來介紹 ES2015 在 Node.js 開發(fā)中的基于 Koa 的一種優(yōu)秀實(shí)踐方式。
因?yàn)?Node.js 自帶模塊機(jī)制,所以用 babel 對(duì) ES2015 的模塊語法做降級(jí)兼容的時(shí)候,只需降至 Node.js 所使用的 CommonJS 標(biāo)準(zhǔn)即可。
不一樣的是,ES2015 的模塊語法是一種聲明式語法,根據(jù) ES2015 中的規(guī)定,模塊引入和暴露都需要在當(dāng)前文件中的最頂層,而不能像 CommonJS 中的 require()
那樣可以在任何地方使用;使用 import
引入的模塊所在命名空間將會(huì)是一個(gè)常量,在 babel 的降級(jí)兼容中會(huì)進(jìn)行代碼檢查,保證模塊命名空間的安全性。
架構(gòu)設(shè)計(jì)
因?yàn)槲覀冞@個(gè) DEMO 的數(shù)據(jù)結(jié)構(gòu)并不復(fù)雜,所以我們可以直接使用 MongoDB 作為我們的后端數(shù)據(jù) 庫,用于存儲(chǔ)我們的文章數(shù)據(jù)。我們可以通過 monk
庫作為我們讀取、操作 MongoDB 的客戶端庫。在架構(gòu)上,Koa 將作為 Web 開發(fā)框架,配合 co 等庫實(shí)現(xiàn)全“同步”的代碼編寫方式。
構(gòu)建應(yīng)用
這就讓我們一步一步來吧,創(chuàng)建 Node.js 應(yīng)用并安裝依賴。
$ npm init
$ npm install koa koa-middlewares koa-static monk co-monk thunkify --save
在上一個(gè)章節(jié)中,我們已經(jīng)建立了整個(gè) DEMO 的文件結(jié)構(gòu),在此我們?cè)僬故疽槐椋?/p>
app
|- src 程序的源文件目錄
| |- controllers 后端的路由處理器
| |- models 數(shù)據(jù)抽象層
| |- lib 后端需要引用的一些庫
| |- public 前端 JavaScript 源文件
| |- app.js 后端程序入口
| |- routes.js 后端路由表
|- dist 降級(jí)兼容輸出目錄
|- gulpfile.js Gulp 構(gòu)建配置文件
|- package.json Node.js 項(xiàng)目配置文件
在 src/controllers
中,包含我們用來相應(yīng)請(qǐng)求的控制器文件;src/lib
文件夾則包含了我們需要的一些抽象庫;src/public
文件則包含了在上一章節(jié)中我們建立的前端應(yīng)用程序;src/app.js
是該應(yīng)用入口文件的源文件;src/routes.js
則是應(yīng)用的路由表文件。
入口文件
在入口文件中,我們需要完成幾件事情:
- 創(chuàng)建 Koa 應(yīng)用,并監(jiān)聽指定端口
- 將所需要的 Koa 中間件接入我們所創(chuàng)建的 Koa 應(yīng)用中,如靜態(tài)資源服務(wù)
- 引入路由表文件,將路由控制器接入我們所建立的 Koa 文件中
import koa from 'koa'
import path from 'path'
import { bodyParser } from 'koa-middlewares'
import Static from 'koa-static'
import router from './routes'
let app = koa()
// Static
app.use(Static(path.resolve(__dirname, './public')))
// Parse the body in POST requests
app.use(bodyParser())
// Router
app.use(router.routes())
let PORT = parseInt(process.env.PORT || 3000)
app.listen(PORT, () => {
console.log(`Demo is running, port:${PORT}`)
})
數(shù)據(jù)抽象層
為了方便我們?cè)?co 的環(huán)境中使用 MongoDB,我們選擇了 monk
和 co-monk
兩個(gè)庫進(jìn)行組合,并抽象出鏈接庫。
// lib/mongo.js
import monk from 'monk'
import wrap from 'co-monk'
import config from '../../config.json'
const db = monk(config.dbs.mongo)
/**
* 返回 MongoDB 中的 Collection 實(shí)例
*
* @param {String} name collection name
* @return {Object} Collection
*/
function collection(name) {
return wrap(db.get(name))
}
export default {
collection
}
通過這個(gè)抽象庫,我們就可以避免每次獲取 MongoDB 中的 Collection 實(shí)例時(shí)都需要連接一遍數(shù)據(jù)庫了。
// models/posts.js
import mongo from '../lib/mongo'
export default mongo.collection('posts')
Posts 控制器
完成了數(shù)據(jù)層的抽象處理后,我們就可以將其用于我們的控制器了,參考 monk
的文檔,我們可以對(duì)這個(gè) Posts Collection 進(jìn)行我們所需要的操作。
import thunkify from 'thunkify'
import request from 'request'
import Posts from '../models/posts'
const requestAsync = thunkify((opts, callback) => {
request(opts, (err, res, body) => callback(err, body))
})
此處我們使用 thunkify
對(duì) request
做了一點(diǎn)小小的封裝工作,而因?yàn)?request
庫自身的 callback 并不是標(biāo)準(zhǔn)的 callback 形式,所以我們并不能直接把 request
函數(shù)傳入 thunkify
中,我們需要的是 callback 中的第三個(gè)參數(shù) body
,所以我們需要自行包裝一層函數(shù)以取得 body
并返回到 co 中。
API:獲取所有文章
我們可以通過這個(gè) API 獲取存儲(chǔ)在 MongoDB 中的所有文章,并支持分頁。支持提供當(dāng)前獲取的頁數(shù),每頁10篇文章,提供每篇文章的標(biāo)題、作者、文章內(nèi)容和評(píng)論。
// GET /api/posts/list?page=0
router.get.listPosts = function*() {
let page = parseInt(this.query.page || 0)
const count = 10
let posts = yield Posts.find({}, {
skip: page * count,
limit: count
})
// 從多說獲取評(píng)論
posts = yield posts.map(post => {
return function*() {
let duoshuoReply = JSON.parse(yield requestAsync(`http://api./threads/listPosts.json?short_name=es2015-in-action&thread_key=${post._id}&page=0&limit=1000`))
var commentsId = Object.keys(duoshuoReply.parentPosts)
post.comments = commentsId.map(id => duoshuoReply.parentPosts[id])
return post
}
})
// 返回結(jié)果
this.body = {
posts: posts
}
}
此處我們用到了 co 的一個(gè)很有意思的特性,并行處理異步操作。我們通過對(duì)從 MongoDB 中取得數(shù)據(jù)進(jìn)行 #map()
方法的操作,返回一組 Generator Functions,并將這個(gè)數(shù)組傳給 yield
,co 便可以將這些 Generator Functions 全部執(zhí)行,并統(tǒng)一返回結(jié)果。同樣的我們還可以使用對(duì)象來進(jìn)行類似的操作:
co(function*() {
let result = yield {
posts: getPostsAsync(),
hot: getHotPosts(),
latestComments: getComments(10)
}
result //=> { posts: [...], hot: [...], latestComments: [...] }
})
API:獲取指定文章
這個(gè) API 用于通過提供指定文章的 ID,返回文章的數(shù)據(jù)。
// GET /api/posts/:id
router.get.getPost = function*() {
let id = this.params.id
let post = yield Posts.findById(id)
let duoshuoReply = JSON.parse(yield requestAsync(`http://api./threads/listPosts.json?short_name=es2015-in-action&thread_key=${id}&page=0&limit=1000`))
var commentsId = Object.keys(duoshuoReply.parentPosts)
post.comments = commentsId.map(id => duoshuoReply.parentPosts[id])
this.body = {
post: post
}
}
API:發(fā)布新文章
相比上面兩個(gè) API,發(fā)布新文章的 API 在邏輯上則要簡單得多,只需要向 Collection 內(nèi)插入新元素,然后將得到的文檔返回至客戶端既可以。
// POST /api/posts/new
router.post.newPost = function*() {
let data = this.request.body
let post = yield posts.insert(data)
this.body = {
post: post
}
}
Comments 控制器
除了文章的 API 以外,我們還需要提供文章評(píng)論的 API,以方便日后該 DEMO 向移動(dòng)端擴(kuò)展和彌補(bǔ)多說評(píng)論框在原生移動(dòng)端上的不足。由于評(píng)論的數(shù)據(jù)并不是存儲(chǔ)在項(xiàng)目數(shù)據(jù)庫當(dāng)中,所以我們也不需要為它創(chuàng)建一個(gè)數(shù)據(jù)抽象層文件,而是直接從控制器入手。
API:獲取指定文章的評(píng)論
// GET /api/comments/post/:id
router.get.fetchCommentsInPost = function*() {
let postId = this.params.id
let duoshuoReply = JSON.parse(yield requestAsync(`http://api./threads/listPosts.json?short_name=es2015-in-action&thread_key=${postId}&page=0&limit=1000`))
let commentsId = Object.keys(duoshuoReply.parentPosts)
let comments = commentsId.map(id => duoshuoReply.parentPosts[id])
this.body = {
comments: comments
}
}
API:發(fā)表新評(píng)論
同樣是為了擴(kuò)展系統(tǒng)的 API,我們通過多說的 API,允許使用 API 來向文章發(fā)表評(píng)論。
// POST /api/comments/post
router.post.postComment = function*() {
let postId = this.request.body.post_id
let message = this.request.body.message
let reply = yield requestAsync({
method: 'POST',
url: `http://api./posts/create.json`,
json: true,
body: {
short_name: duoshuo.short_name,
secret: duoshuo.secret,
thread_key: postId,
message: message
}
})
this.body = {
comment: reply.response
}
}
配置路由
完成控制器的開發(fā)后,我們是時(shí)候把路由器跟控制器都連接起來了,我們?cè)?src/routes.js
中會(huì)將所有控制器都綁定到路由上,成為一個(gè)類似于路由表的文件。
import { router as Router } from 'koa-middlewares'
首先,我們要將所有的控制器引入到路由文件中來。
import posts from './controllers/posts'
import comments from './controllers/comments'
然后,創(chuàng)建一個(gè)路由器實(shí)例,并將所有控制器的響應(yīng)器和 URL 規(guī)則一一綁定。
let router = new Router()
// Posts
router.get('/api/posts/list', posts.get.listPosts)
router.get('/api/posts/:id', posts.get.getPost)
router.post('/api/posts/new', posts.post.newPost)
// Comments
router.get('/api/comments/post/:id', comments.get.fetchCommentsInPost)
router.post('/api/comments/post', comments.post.postComment)
export default router
配置任務(wù)文件
經(jīng)過對(duì)數(shù)據(jù)抽象層、邏輯控制器、路由器的開發(fā)后,我們便可以將所有代碼利用 Gulp 進(jìn)行代碼構(gòu)建了。
我們先安裝好所需要的依賴庫。
$ npm install gulp gulp-babel vinyl-buffer vinyl-source-stream babelify browserify --save-dev
$ touch gulpfile.js
很可惜的是,Gulp 原生并不支持 ES2015 標(biāo)準(zhǔn)的代碼,所以在此我們也只能通過 ES5 標(biāo)準(zhǔn)的代碼編寫任務(wù)文件了。
'use strict'
var gulp = require('gulp')
var browserify = require('browserify')
var babel = require('gulp-babel')
var babelify = require('babelify')
var source = require('vinyl-source-stream')
var buffer = require('vinyl-buffer')
var babel = require('gulp-babel')
我們主要需要完成兩個(gè)構(gòu)建任務(wù):
- 編譯并構(gòu)建前端 JavaScript 文件
- 編譯后端 JavaScript 文件
在構(gòu)建前端 JavaScript 文件的過程中,我們需要利用 Browserify 配合 babelify 進(jìn)行代碼編譯和合并。
gulp.task('browserify', function() {
return browserify({
cache: {},
packageCache: {},
entries: ['./src/public/main.js']
})
.transform(babelify.configure({
presets: [ 'es2015-without-regenerator' ],
plugins: [ 'transform-async-to-generator' ]
}))
.bundle()
.pipe(source('bundle.js'))
.pipe(buffer())
.pipe(gulp.dest('dist/public'))
})
我們將代碼編譯好以后,便將其復(fù)制到 dist/public
文件夾中,這也是我們 Node.js 后端處理靜態(tài)資源請(qǐng)求的響應(yīng)地址。
而構(gòu)建后端代碼則更為簡單,因?yàn)槲覀冎恍枰獙⑵渚幾g并復(fù)制到 dist
文件夾即可。
gulp.task('babel-complie', function() {
return gulp.src('src/**/*.js')
.pipe(babel({
presets: [ 'es2015-without-regenerator' ],
plugins: [ 'transform-async-to-generator' ]
}))
.pipe(gulp.dest('dist/'))
})
好了,在完成 gulpfile.js 文件的編寫以后,我們就可以進(jìn)行代碼構(gòu)建了。
$ gulp
[07:30:27] Using gulpfile ~/path/to/app/gulpfile.js
[07:30:27] Starting 'babel-complie'...
[07:30:27] Starting 'browserify'...
[07:30:29] Finished 'babel-complie' after 2.35 s
[07:30:30] Finished 'browserify' after 3.28 s
[07:30:30] Starting 'default'...
[07:30:30] Finished 'default' after 29 μs
最后,我們就可以利用 dist
文件夾中已經(jīng)編譯好的代碼運(yùn)行起來了。
$ node dist/app.js
Demo is running, port:3000
不出意外,我們就可以看到我們想要的效果了。
前方高能反應(yīng)
部署到 DaoCloud
完成了代碼的開發(fā) ,我們還需要將我們的項(xiàng)目部署到線上,讓別人看到我們的成果~
Docker 是目前最流行的一種容器化應(yīng)用搭建工具,我們可以通過 Docker 快速地將我們的應(yīng)用部署在任何支持 Docker 的地方,哪怕是 Raspberry Pi 還是公司的主機(jī)上。
而 DaoCloud 則是目前國內(nèi)做 Docker 商業(yè)化體驗(yàn)最好的公司,他們提供了一系列幫助開發(fā)者和企業(yè)快速使用 Docker 進(jìn)行項(xiàng)目部署的工具。這里我們將介紹如何將我們的 DEMO 部署到 DaoCloud 的簡易容器中。
Dockerfile
在使用 Docker 進(jìn)行項(xiàng)目部署前,我們需要在項(xiàng)目中新建一個(gè) Dockerfile 以表達(dá)我們的鏡像構(gòu)建任務(wù)。
因?yàn)槲覀冇玫氖?Node.js 作為項(xiàng)目基礎(chǔ)運(yùn)行環(huán)境,所以我們需要從 Node.js 的官方 Docker 鏡像中引用過來。
FROM node:onbuild
因?yàn)槲覀円呀?jīng)在 package.json
中寫好了對(duì) gulp
的依賴,這樣我們就可以直接在 docker build
的時(shí)候?qū)Υa進(jìn)行編譯。
RUN ./node_modules/.bin/gulp
此外,我們還需要安裝另外一個(gè)依賴庫 pm2
,我們需要使用 pm2
作為我們項(xiàng)目的守護(hù)程序。
$ npm install pm2 --save-dev
然后,我們簡單地向 Docker 的宿主機(jī)器申請(qǐng)暴露 80 端口,并利用 pm2
啟動(dòng) Node.js 程序。
EXPOSE 80
CMD ./node_modules/.bin/pm2 start dist/app.js --name ES2015-In-Action --no-daemon
至此,我們已經(jīng)完成了 Dockerfile 的編寫,接下來就可以將項(xiàng)目代碼上傳到 GitHub 等地方,供 DaoCloud 使用了。這里我不再說明 Git 和 GitHub 的使用。

創(chuàng)建 DaoCloud 上的 MongoDB 服務(wù)
借助于 Docker 的強(qiáng)大擴(kuò)容性,我們可以在 DaoCloud 上很方便地創(chuàng)建用于項(xiàng)目的 MongoDB 服務(wù)。
在“服務(wù)集成”標(biāo)簽頁中,我們可以選擇部署一個(gè) MongoDB 服務(wù)。



創(chuàng)建好 MongoDB 服務(wù)后,我們就要將我們上傳到 GitHub 的項(xiàng)目代碼利用 DaoCloud 進(jìn)行鏡像構(gòu)建了。
代碼構(gòu)建
DaoCloud 提供了一個(gè)十分方便的工具,可以把我們存儲(chǔ)在 GitHub、BitBucket、GitCafe、Coding 等地方的項(xiàng)目代碼,通過其中的 Dockerfile 構(gòu)建成一個(gè) Docker 鏡像,用于部署到管理在 DaoCloud 上的容器。

通過綁定 GitHub 賬號(hào),我們可以選擇之前發(fā)布在 GitHub 上的項(xiàng)目,然后拉取到 DaoCloud 中。


構(gòu)建完成以后,我們就可以在“鏡像倉庫”中看到我們的項(xiàng)目鏡像了。

我們將其部署到一個(gè) DaoCloud 的容器中,并且把它與之前創(chuàng)建的 MongoDB 服務(wù)綁定。


最后的最后,我們點(diǎn)擊“立即部署”,等待部署成功就可以看到我們的項(xiàng)目線上狀態(tài)了。

我們可以在這里看到我部署的 DEMO:http://es2015-demo./
而我們的 DEMO,可以在這里查看詳細(xì)代碼:https://github.com/iwillwen/es2015-demo
至此,我們已經(jīng)完成了 Node.js 端和前端的構(gòu)建,并且將其部署到了 DaoCloud 上,以供瀏覽。下面,我們?cè)賮砜纯?,正在發(fā)展中的 ES7 又能給我們帶來什么驚喜吧。
一窺 ES7
async/await
上文中我們提及到 co 是一個(gè)利用 Generator 模擬 ES7 中 async/await
特性的工具,那么,這個(gè) async/await
究竟又是什么呢?它跟 co 又有什么區(qū)別呢?
我們知道,Generator Function 與普通的 Function 在執(zhí)行方式上有著本質(zhì)的區(qū)別,在某種意義上是無法共同使用的。但是,對(duì)于 ES7 的 Async Function 來說,這一點(diǎn)并不存在!它可以以普通函數(shù)的執(zhí)行方式使用,并且有著 Generator Function 的異步優(yōu)越性,它甚至可以作為事件響應(yīng)函數(shù)使用。
async function fetchData() {
let res = await fetch('/api/fetch/data')
let reply = await res.json()
return reply
}
var reply = fetchData() //=> DATA...
遺憾的是,async/await
所支持的并不如 co 多,如并行執(zhí)行等都暫時(shí)沒有得到支持。
Decorators
對(duì)于 JavaScript 開發(fā)者來說,Decorators 又是一種新的概念,不過它在 Python 等語言中早已被玩出各種花式。
Decorator 的定義如下:
- 是一個(gè)表達(dá)式
- Decorator 會(huì)調(diào)用一個(gè)對(duì)應(yīng)的函數(shù)
- 調(diào)用的函數(shù)中可以包含
target
(裝飾的目標(biāo)對(duì)象)、name
(裝飾目標(biāo)的名稱)和 descriptor
(描述器)三個(gè)參數(shù)
- 調(diào)用的函數(shù)可以返回一個(gè)新的描述器以應(yīng)用到裝飾目標(biāo)對(duì)象上
PS:如果你不記得 descriptor
是什么的話,請(qǐng)回顧一下 Object.defineProperty()
方法。
簡單實(shí)例
我們?cè)趯?shí)現(xiàn)一個(gè)類的時(shí)候,有的屬性并不想被 for..in
或 Object.keys()
等方法檢索到,那么在 ES5 時(shí)代,我們會(huì)用到 Object.defineProperty()
方法來實(shí)現(xiàn):
var obj = {
foo: 1
}
Object.defineProperty(obj, 'bar', {
enumerable: false,
value: 2
})
console.log(obj.bar) //=> 2
var keys = []
for (var key in obj)
keys.push(key)
console.log(keys) //=> [ 'foo' ]
console.log(Object.keys(obj)) //=> [ 'foo' ]
那么在 ES7 中,我們可以用 Decorator 來很簡單地實(shí)現(xiàn)這個(gè)需求:
class Obj {
constructor() {
this.foo = 1
}
@nonenumerable
get bar() { return 2 }
}
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false
return descriptor
}
var obj = new Obj()
console.log(obj.foo) //=> 1
console.log(obj.bar) //=> 2
console.log(Object.keys(obj)) //=> [ 'foo' ]
黑科技
正如上面所說,Decorator 在編程中早已不是什么新東西,特別是在 Python 中早已被玩出各種花樣。聰明的工程師們看到 ES7 的支持當(dāng)然不會(huì)就此收手,就讓我們看看我們還能用 Decorator 做點(diǎn)什么神奇的事情。
假如我們要實(shí)現(xiàn)一個(gè)類似于 Koa 和 PHP 中的 CI 的框架,且利用 Decorator 特性實(shí)現(xiàn) URL 路由,我們可以這樣做。
// 框架內(nèi)部
// 控制器
class Controller {
// ...
}
var handlers = new WeakMap()
var urls = {}
// 定義控制器
@route('/')
class HelloController extends Controller {
constructor() {
super()
this.msg = 'World'
}
async GET(ctx) {
ctx.body = `Hello ${this.msg}`
}
}
// Router Decorator
function route(url) {
return target => {
target.url = url
let urlObject = new String(url)
urls[url] = urlObject
handlers.set(urlObject, target)
}
}
// 路由執(zhí)行部份
function router(url) {
if (urls[url]) {
var handlerClass = handlers.get(urls[url])
return new handlerClass()
}
}
var handler = router('/')
if (handler) {
let context = {}
handler.GET(context)
console.log(context.body)
}
最重要的是,同一個(gè)修飾對(duì)象是可以同時(shí)使用多個(gè)修飾器的,所以說我們還可以用修飾器實(shí)現(xiàn)很多很多有意思的功能。
后記
對(duì)于一個(gè)普通的 JavaScript 開發(fā)者來說,ES2015 可能會(huì)讓人覺得很模糊和難以學(xué)習(xí),因?yàn)?ES2015 中帶來了許多我們從前沒有在 JavaScript 中接觸過的概念和特性。但是經(jīng)過長時(shí)間的考察,我們不難發(fā)現(xiàn) ES2015 始終是 JavaScript 的發(fā)展方向,這是不可避免的。因此我要在很長一段時(shí)間內(nèi)都向身邊的或是社區(qū)中的 JavaScript 開發(fā)者推廣 ES2015,推薦他們使用最新的技術(shù)。
這篇文章說長不長,說短也不短,我只能在有限的文字篇幅內(nèi)盡可能把更多的知識(shí)展示出來,更深入的細(xì)節(jié)還需要讀者自行探索。無論如何,若是這篇文章能引起各位 JavaScript 開發(fā)者對(duì) ES2015 的興趣和重視,并且中從學(xué)會(huì)了如何在項(xiàng)目中使用 ES2015 標(biāo)準(zhǔn)進(jìn)行開發(fā),那么這篇文章的目的就已經(jīng)達(dá)到了。
再次感謝對(duì)這篇文章的寫作提供了支持的各位(名次均不分先后):
審讀大牛團(tuán):代碼家, 樸靈, 寒冬winter, TooBug, 郭達(dá)峰, 芋頭, 尤雨溪, 張?jiān)讫?/a>, 民工精髓V
內(nèi)測(cè)讀者:死月, 米粽, 陰明, 引證, 老雷, Jonah, Crzidea, 送送
贊助方:DaoCloud, 100Offer
PS:我廠準(zhǔn)備在100offer招人了!聽說技術(shù)牛人都上他們網(wǎng)站找工作!
關(guān)于作者
甘超陽(小問),LikMoon(離門創(chuàng)造)創(chuàng)始人
GitHub: Will Wen Gunn
微博:端木文_Wen
Twitter:@iwillwen
Facebook:Will Wen Gunn
博客地址:Life Map
歡迎大家加入我的文章陪審團(tuán),以后有新的文章都可以率先閱讀哦~