这篇文章本来是繁体中文的,我把它转成简体放在这里。原文是http://lifemap.in/es2015-in-action/,对于Javascript ES6 介绍的比较全面。


前言

历时将近6年的时间来制定的新ECMAScript标准ECMAScript 6(亦称ECMAScript Harmony,简称ES6)终于在2015年6月正式发布。自从上一个标准版本ES5在2009年发布以后,ES6就一直以新语法新特性的优越性吸引着众多JavaScript开发者,驱使他们积极尝鲜。

虽然至今各大浏览器厂商所开发的JavaScript引擎都还没有完成对ES2015中所有特性的完美支持,但这并不能阻挡工程师们对ES6的热情,于是乎如babel、Traceur等编译器便出现了。它们能将尚未得到支持的ES2015特性转换为ES5标准的代码,使其得到浏览器的支持。其中,babel因其模块化转换器(Transformer)的设计特点赢得了绝大部份JavaScript开发者的青睐,本文也将以babel为基础工具,向大家展示ES2015的神奇魅力。

笔者目前所负责的项目中,已经在前端和后端全方位的使用了ES2015 标准进行JavaScript 开发,已有将近两年的ES2015 开发经验。如今ES2015 以成为ECMA 国际委员会的首要语言标准,使用ES2015 标准所进行的工程开发已打好了坚实的基础,而ES7(ES2016) 的定制也走上了正轨,所以在这个如此恰当的时机,我觉得应该写一篇通俗易懂的ES2015 教程来引导广大JavaScript 爱好者和工程师向新时代前进。若您能从本文中有所收获,便是对我最大的鼓励。

我希望你在阅读本文前,已经掌握了JavaScript 的基本知识,并具有一定的Web App 开发基础和Node.js 基本使用经验。

目录

  1. 一言蔽之ES2015
  2. ES2015 能为JavaScript 的开发带来什么 
    1. 语法糖
    2. 工程优势
  3. ES2015 新语法详解 
    1. letconst和块级作用域
      • 块级作用域
    2. 箭头函数(Arrow Function)
      • 使用方法
      • 箭头函数与上下文绑定
      • 注意事项
    3. 模板字符串
      • 支持变量注入
      • 支持换行
    4. 对象字面量扩展语法
      • 方法属性省略 function
      • 支持注入 __proto__
      • 同名方法属性省略语法
      • 可以动态计算的属性名称
    5. 表达式解构
    6. 函数参数表达、传参
      • 默认参数值
      • 后续参数
      • 注意事项
      • 解构传参
      • 注意事项
    7. 新的数据结构
      • Set 和WeakSet
      • Map 和WeakMap
    8. 类(Classes)
      • 语法
      • 定义
      • 继承
      • 静态方法
      • 遗憾与期望
    9. 生成器(Generator)
      • 来龙
      • 基本概念
      • Generator Function
      • Generator
      • 基本使用方法
    10. Promise
      • 概念
      • 基本用法
      • 弊端
    11. 原生的模块化
      • 历史小回顾
      • 基本用法
        • 全局引入
        • 局部引入
        • 接口暴露
      • 降级兼容
    12. Symbol
      • 黑科技
    13. Proxy(代理)
  4. ES2015 的前端开发实战 
    1. 构建界面
    2. 结构定义
    3. 架构设计
      • 模块化
      • 数据支持
      • 界面渲染
    4. 构建应用
      • 入口文件
      • 数据层:文章
      • 路由:首页
      • 准备页面渲染
      • 加载数据
      • 设计组件
      • 路由:文章页面
      • 路由:发布新文章
    5. 路由绑定
    6. 合并代码
  5. ES2015 的Node.js 开发实战 
    1. 架构设计
    2. 构建应用
      • 入口文件
      • 数据抽象层
      • Posts 控制器
      • API:获取所有文章
      • API:获取指定文章
      • API:发布新文章
      • Comments 控制器
      • API:获取指定文章的评论
      • API:发表新评论
      • 配置路由
    3. 配置任务文件
    4. 部署到DaoCloud
      • Dockerfile
      • 创建DaoCloud 上的MongoDB 服务
      • 代码构建
  6. 一窥ES7 
    1. async/await
    2. Decorators
      • 简单实例
      • 黑科技
  7. 后记

本文的实战部份将以开发一个动态博客系统为背景,向大家展示如何使用ES2015 进行项目开发。成品代码将在GitHub 上展示。


一言蔽之ES2015

说到ES2015,有了解过的同学一定会马上想到各种新语法,如箭头函数(=>)、class、模板字符串等。是的,ECMA委员会吸取了许多来自全球众多JavaScript开发者的意见和来自其他优秀编程语言的经验,致力于制定出一个更适合现代JavaScript开发的标准,以达到“和谐”(Harmony)。一言蔽之:

ES2015 标准提供了许多新的语法和编程特性以提高JavaScript 的开发效率和体验

从ES6的别名被定为Harmony开始,就注定了这个新的语言标准将以一种更优雅的姿态展现出来,以适应日趋复杂的应 ​​用开发需求。

ES2015 能为JavaScript 的开发带来什么

语法糖

如果您有其他语言(如Ruby、Scala)或是某些JavaScript的衍生语言(如CoffeeScript、TypeScript)的开发经验,就一定会了解一些很有意思的语法糖,如Ruby中的Range -> 1..10,Scala和CoffeeScript中的箭头函数(a, b) => a + b。ECMA委员会借鉴了许多其他编程语言的标准,给ECMAScript家族带来了许多可用性非常高的语法糖,下文将会一一讲解。

这些语法糖能让JavaScript 开发者更舒心地开发JavaScript 应用,提高我们的工作效率,多一些时间出去浪

工程优势

ES2015除了提供了许多语法糖以外,还由官方解决了多年来困扰众多JavaScript开发者的问题:JavaScript的模块化构建。从许多年前开始,各大公司、团队、大牛都相继给出了他们对于这个问题的不同解决方案,以至于定下了如CommonJS、AMD、CMD或是UMD等JavaScript模块化标准,RequireJS、SeaJS、FIS、Browserify、webpack等模块加载库都以各自不同的优势占领着一方土地。

然而正正是因为这春秋战国般的现状,广大的前端搬砖工们表示很纳闷。

这™究竟哪种好?哪种适合我?求大神带我飞!

对此,ECMA 委员会终于是坐不住了,站了起来表示不服,并制订了ES2015 的原生模块加载器标准。

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

老实说,这套模块化语法不禁让我们又得要对那个很silly 的问题进行重新思考了:JavaScript 和Java 有什么关系?

可惜的是,目前暂时还没有任何浏览器厂商或是JavaScript 引擎支持这种模块化语法。所以我们需要用babel 进行转换为CommonJS、AMD 或是UMD 等模块化标准的语法。

ES2015 新语法详解

经过以上的介(xun)绍(tao),相信你对ES2015 也有了一定的了解和期待。接下来我将带大家慢慢看看ECMA 委员会含辛茹苦制定的新语言特性吧。

letconst和块级作用域

在ES2015的新语法中,影响速度最为直接,范围最大的,恐怕得数letconst了,它们是继var之后,新的变量定义方法。与let相比,const更容易被理解:const也就是constant的缩写,跟C/C++等经典语言一样,用于定义常量,即不可变量。

但由于在ES6之前的ECMAScript标准中,并没有原生的实现,所以在降级编译中,会马上进行引用检查,然后使用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'
...

块级作用域

在ES6 诞生之前,我们在给JavaScript 新手解答困惑时,经常会提到一个观点:

JavaScript 没有块级作用域

在ES6 诞生之前的时代中,JavaScript 确实是没有块级作用域的。这个问题之所以为人所熟知,是因为它引发了诸如历遍监听事件需要使用闭包解决等问题。

<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>

前端新手非常容易写出类似的代码,因为从直观的角度看这段代码并没有语义上的错误,但是当我们点击任意一个按钮时,就会报出这样的错误信息:

Uncaught TypeError: Cannot read property 'innerText' of undefined

出现这个错误的原因是因为buttons[i]不存在,即为undefined

为什么会出现按钮不存在结果呢?通过排查,我们可以发现,每次我们点击按钮时,事件监听回调函数中得到的变量i都会等于buttons.length,也就是这里的4。而buttons[4]恰恰不存在,所以导致了错误的发生。

再而导致i得到的值都是buttons.length的原因就是因为JavaScript中没有块级作用域,而使对i的变量引用(Reference)一直保持在上一层作用域(循环语句所在层)上,而当循环结束时i则正好是buttons.length

而在ES6 中,我们只需做出一个小小的改动,便可以解决该问题(假设所使用的浏览器已经支持所需要的特性):

// ...
for (/* var */ let i = 0; i < buttons.length; i++) {
  // ...
}
// ...

通過把 `for` 語句中對計數器 `i` 的定義語句從 `var` 換成 `let`,即可。因為 `let` 語句會使該變量處於一個塊級作用域中,從而讓事件監聽回調函數中的變量引用得到保持。我們不妨看看改進后的代碼經過 babel 的編譯會變成什麼樣子:

// ...
var _loop = function (i) {
  buttons[i].addEventListener('click', function () {
    output.innerText = buttons[i].innerText
  })
}

for (var i = 0; i < buttons.length; i++) {
  _loop(i)
}
// ...

实现方法一目了然,通过传值的方法防止了i的值错误。

箭头函数(Arrow Function)

letconst之后,箭头函数就是使用率最高的新特性了。当然了,如果你了解过Scala或者曾经如日中天的JavaScript衍生语言CoffeeScript,就会知道箭头函数并非ES6独创。

箭头函数,顾名思义便是使用箭头( =>)进行定义的函数,属于匿名函数(Lambda)一类。当然了,也可以作为定义式函数使用,但我们并不推荐这样做,随后会详细解释。

使用

箭头函数有好几种使用语法:

1. foo => foo + ' world'    // means return `foo + ' world'`
2. (foo, bar) => foo + bar
3. 
foo => {
  return foo + ' world'
}
4.
(foo, bar) => {
  return foo + bar
}

以上都是被支持的箭头函数表达方式,其最大的好处便是简洁明了,省略了function关键字,而使用=>代替。

箭头函数语言简洁的特点使其特别适合用于单行回调函数的定义:

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的开发经验,就一定会觉得这非常亲切,因为这跟其中的RDD操作几乎如出一辙:

  1. 将原本的由名字组成的数组转换为一个格式为{ id, name }的对象,id则为每个名字在原数组中的位置
  2. 剔除其中id为奇数的元素,只保留id为偶数的元素
  3. 将剩下的元素转换为一个包含当前元素中原名字的单元数组,以方便下一步的处理
  4. 通过不断合并相邻的两个数组,最后能得到的一个数组,便是我们需要得到的目标值

箭头函数与上下文绑定

事实上,箭头函数在ES2015标准中,并不只是作为一种新的语法出现。就如同它在CoffeeScript中的定义一般,是用于对函数内部的上下文(this)绑定为定义函数所在的作用域的上下文。

let obj = {
  hello: 'world',
  foo() {
    let bar = () => {
      return this.hello
    }
    return bar
  }
}

window.hello = 'ES6'
window.bar = obj.foo()
window.bar() //=> 'world'

上面代码中的obj.foo等价于(不代表等于):

// ...
foo() {
  let bar = (function() {
    return this.hello
  }).bind(this)

  return bar
}
// ...

为什么要为箭头函数给予这样的特性呢?我们可以假设出这样的一个应用场景,我们需要创建一个实例,用于对一些数据进行查询和筛选。

let DataCenter = {
  baseUrl: 'http://example/api/data',
  search(query) {
    fetch(`${this.baseUrl}/search?query=${query}`)
      .then(res => res.json())
      .then(rows => {
        // TODO
      })
  }
}

此时,从服务器获得数据是一个JSON编码的数组,其中包含的元素是若干元素的ID,我们需要另外请求服务器的其他API以获得元素本身(当然了,实际上的API设计大部份不会这么使用这么蛋疼的设计)。我们就需要在回调函数中再次使用this.baseUrl这个属性,如果要同时兼顾代码的可阅读性和美观性,ES2015允许我们这样做。

let DataCenter = {
  baseUrl: 'http://example/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 中的某個實例
      })
      .then(res => res.json())
  }
}

DataCenter.search('iwillwen')
  .then(rows => console.log(rows))

因为在单行匿名函数中,如果this指向的是该函数的上下文,就会不符合直观的语义表达。

注意事项

另外,要注意的是,箭头函数对上下文的绑定是强制性的,无法通过applycall方法改变其上下文。

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

另外,因为箭头函数会绑定上下文的特性,故不能随意在顶层作用域使用箭头函数,以防出错:

// 假設當前運行環境為瀏覽器,故頂層作上下文為 `window`
let obj = {
  msg: 'pong',

  ping: () => {
    return this.msg // Warning!
  }
}

obj.ping() //=> undefined
let msg = 'bang!'
obj.ping() //=> bang!

为什么上面这段代码会如此让人费解呢?

我们来看看它的等价代码吧。

let obj = {
  // ...
  ping: (function() {
    return this.msg // Warning!
  }).bind(this)
}

// 同樣等價于
let obj = { /* ... */ }
obj.ping = (function() {
  return this.msg
}).bind(this /* this -> window */)

模板字符串

模板字符串模板出现简直对Node.js 应用的开发和Node.js 自身的发展起到了相当大的推动作用!我的意思并不是说这个原生的模板字符串能代替现有的模板引擎,而是说它的出现可以让非常多的字符串使用变得尤为轻松。

模板字符串要求使用` 代替原本的单/双引号来包裹字符串内容。它有两大特点:

  1. 支持变量注入
  2. 支持换行

支持变量注入

模板字符串之所以称之为“模板”,就是因为它允许我们在字符串中引用外部变量,而不需要像以往需要不断地相加、相加、相加……

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(' ')

无论是上面的哪一种,都会让我们感到很不爽。但若使用模板字符串,仿佛打开了新世界的大门~

let sql = `
SELECT * FROM Users
WHERE FirstName='Mike'
LIMIT 5;
`

Sweet! 在Node.js 应用的实际开发中,除了SQL 的编写,还有如Lua 等嵌入语言的出现(如Redis 中的SCRIPT 命令),或是手工的XML 拼接。模板字符串的出现使这些需求的解决变得不再纠结了~

对象字面量扩展语法

看到这个标题的时候,相信有很多同学会感到奇怪,对象字面量还有什么可以扩展的?

确实,对象字面量的语法在ES2015 之前早已挺完善的了。不过,对于聪明的​​工程师们来说,细微的改变,也能带来不少的价值。

方法属性省略 function

这个新特性可以算是比较有用但并不是很显眼的一个。

let obj = {
  // before
  foo: function() {
    return 'foo'
  },

  // after
  bar() {
    return 'bar'
  }
}

支持__proto__注入

在ES2015中,我们可以给一个对象硬生生的赋予其__proto__,这样它就可以成为这个值所属类的一个实例了。

class Foo {
  constructor() {
    this.pingMsg = 'pong'
  }

  ping() {
    console.log(this.pingMsg)
  }
}

let o = {
  __proto__: new Foo()
}

o.ping() //=> pong

什么?有什么卵用?

有一个比较特殊的场景会需要用到:我想扩展或者覆盖一个类的方法,并生成一个实例,但觉得另外定义一个类就感觉浪费了。那我可以这样做:

let o = {
  __proto__: new Foo(),

  init() {
    this.pingMsg = 'alive'
  },

  msg: 'bang',
  yell() {
    console.log(this.msg)
  }
}

o.init()
o.yell() //=> bang
o.ping() //=> alive

同名方法属性省略语法

也是看上去有点鸡肋的新特性,不过在做JavaScript 模块化工程的时候则有了用武之地。

// module.js
export default {
  someMethod
}

function someMethod() {
  // ...
}

// app.js
import Module from './module'

Module.someMethod()

可以动态计算的属性名称

这个特性相当有意思,也是可以用在一些特殊的场景中。

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 }
  ]

在上面的两个[...]中,我演示了动态计算的对象属性名称的使用,分别为对应的对象定义了当前计数器nn的2次方

表达式解构

来了来了来了,相当有用的一个特性。有啥用?多重复值听过没?没听过?来看看吧!

// 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})

还有一些可用性不大,但也是有一点用处的:

// Fail-soft destructuring
var [a] = []
a === undefined //=> true

// Fail-soft destructuring with defaults
var [a = 1] = []
a === 1 //=> true

函数参数表达、传参

这个特性有非常高的使用频率,一个简单的语法糖解决了从前需要一两行代码才能实现的功能。

默认参数值

这个特性在类库开发中相当有用,比如实现一些可选参数:

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 => {
  // ...
})

后续参数

我们知道,函数的callapply在使用上的最大差异便是一个在首参数后传入各个参数,一个是在首参数后传入一个包含所有参数的数组。如果我们在实现某些函数或方法时,也希望实现像call一样的使用方法,在ES2015之前,我们可能需要这样做:

function fetchSomethings() {
  var args = [].slice.apply(arguments)

  // ...
}
function doSomeOthers(name) {
  var args = [].slice.apply(arguments, 1)

  // ...
}

而在 ES2015 中,我們可以很簡單的使用 `…` 語法糖來實現:

function fetchSomethings(...args) {
  // ...
}
function doSomeOthers(name, ...args) {
  // ...
}

要注意的是,...args后不可再添加

虽然从语言角度看,arguments...args是可以同时使用,但有一个特殊情况则不可:arguments在箭头函数中,会跟随上下文绑定到上层,所以在不确定上下文绑定结果的情况下,尽可能不要再箭头函数中再使用arguments,而使用...args

虽然ECMA委员会和各类编译器都无强制性要求用...args代替arguments,但从实践经验看来,...args确实可以在绝大部份场景下可以代替arguments使用,除非你有很特殊的场景需要使用到arguments.calleearguments.caller。所以我推荐都使用...args而非arguments

PS:在严格模式(Strict Mode)中,arguments.calleearguments.caller是被禁止使用的。

解构传参

在ES2015中,...语法还有另外一个功能:无上下文绑定的apply。什么意思?看看代码你就知道了。

function sum(...args) {
  return args.map(Number)
    .reduce((a, b) => a + b)
}

console.log(sum(...[1, 2, 3])) //=> 6

有什么卵用?我也不知道(⊙o⊙)... Sorry...

注意事项

默认参数值后续参数需要遵循顺序原则,否则会出错。

function(...args, last = 1) {
  // This will go wrong
}

另外,根据函数调用的原则,无论是默认参数值还是后续参数都需要小心使用。

新的数据结构

在介绍新的数据结构之前,我们先复习一下在ES2015 之前,JavaScript 中有哪些基本的数据结构。

  • String 字符串
  • Number 数字(包含整型和浮点型)
  • Boolean 布尔值
  • Object 对象
  • Array 数组

其中又分为值类型引用类型,Array其实是Object的一种子类。

Set 和WeakSet

我们再来复习下高中数学吧,集不能包含相同的元素,我们可以根据元素画出多个集的韦恩图…………

好了跑题了。是的,在ES2015 中,ECMA 委员会为ECMAScript 增添了集(Set)和“弱”集(WeakSet)。它们都具有元素唯一性,若添加了已存在的元素,会被自动忽略。

let s = new Set()
s.add('hello').add('world').add('hello')
console.log(s.size) //=> 2
console.log(s.has('hello')) //=> true

在实际开发中,我们有很多需要用到集的场景,如搜索、索引建立等。

咦?怎么还有一个WeakSet?这是干什么的?我曾经写过一篇关于JavaScript内存优化的文章,而其中大部份都是在语言上动手脚,而WeakSet则是在数据上做文章。

WeakSet在JavaScript底层作出调整(在非降级兼容的情况下),检查元素的变量引用情况。如果元素的引用已被全部解除,则该元素就会被删除,以节省内存空间。这意味着无法直接加入数字或者字符串。另外WeakSet对元素有严格要求,必须是Object,当然了,你也可以用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

从数据结构的角度来说,映射(Map)跟原本的Object 非常相似,都是Key/Value 的键值对结构。但是Object 有一个让人非常不爽的限制:key 必须是字符串或数字。在一般情况下,我们并不会遇上这一限制,但若我们需要建立一个对​​象映射表时,这一限制显得尤为棘手。

而Map 则解决了这一问题,可以使用任何对象作为其key,这可以实现从前不能实现或难以实现的功能,如在项目逻辑层实现数据索引等。

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 的键和值都会检查变量引用,只要其一的引用全被解除,该键值对就会被删除。

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诞生以来最大的痛点之一,终于在ES2015中得到了官方的妥协,“实现”了ECMAScript中的标准类机制。为什么是带有双引号的呢?因为我们不难发现这样一个现象:

$ node
> class Foo {}
[Function: Foo]

回想一下在ES2015 以前的时代中,我们是怎么在JavaScript 中实现类的?

function Foo() {}
var foo = new Foo()

是的,ES6中的类只是一种语法糖,用于定义原型(Prototype)的。当然,饿死的厨师三百斤,有总比没有强,我们还是很欣然地接受了这一设定。

语法

定义

与大多数人所期待的一样,ES2015 所带来的类语法确实与很多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 中的对象字面量不一样的是,类的属性后不能加逗号,而对象字面量则必须要加逗号。

然而,让人很不爽的是,ES2015 中对类的定义依然不支持默认属性的语法:

// 理想型
class Person {
  name: String
  gender = 'man'
  // ...
}

而在TypeScript 中则有良好的实现。

继承

ES2015 的类继承总算是为JavaScript 类继承之争抛下了一根定海神针了。在此前,有各种JavaScript 的继承方法被发明和使用。(详细请参见《JavaScript 高级程序设计》)

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中若要是一个类继承于另外一个类而作为其子类,只需要在子类的名字后面加上extends {SuperClass}即可。

静态方法

ES2015中的类机制支持static类型的方法定义,比如说Man是一个类,而我希望为其定义一个Man.isMan()方法以用于类型检查,我们可以这样做:

class Man {
  // ...

  static isMan(obj) {
    return obj instanceof Man
  }
}

let me = new Man()
console.log(Man.isMan(me)) //=> true

遗憾的是,ES2015的类并不能直接地定义静态成员变量,但若必须实现此类需求,可以用static加上get语句和set语句实现。

class SyncObject {
  // ...

  static get baseUrl() {
    return 'http://example/api/sync'
  }
}

遗憾与期望

就目前来说,ES2015 的类机制依然很鸡肋:

  1. 不支持私有属性(private
  2. 不支持前置属性定义,但可用get语句和set语句实现
  3. 不支持多重继承
  4. 没有类似于协议(Protocl)或接口(Interface)等的概念

中肯地说,ES2015 的类机制依然有待加强。但总的来说,是值得尝试和讨论的,我们可以像从前一样,不断尝试新的方法,促进ECMAScript 标准的发展。

生成器(Generator)

终于到了ES2015 中我最喜欢的特性了,前方高能反应,所有人立刻进入战斗准备!

为什么说这是我最喜欢的新特性呢?对于一个纯前端的JavaScript 工程师来说,可能Generator 并没有什么卵用,但若你曾使用过Node.js 或者你的前端工程中有大量的异步操作,Generator 简直是你的“贤者之石”。(不过,这并不是Generator 最正统的用法。出于严谨,我会从头开始讲述Generator)

来龙

Generator 的设计初衷是为了提供一种能够简便地生成一系列对象的方法,如计算斐波那契数列(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,你一定会对这段代码感到很奇怪:为什么function后会有一个*?为什么函数里使用了while (true)却没有进入死循环而导致死机?yield又是什么鬼?

不着急,我们一一道来。

基本概念

在学习如何使用Generator 之前,我们先了解一些必要的概念。

Generator Function

生成器函数用于生成生成器(Generator),它与普通函数的定义方式的区别就在于它需要在function后加一个*

function* FunctionName() {
  // ...Generator Body
}

生成器函数的声明形式不是必须的,同样可以使用匿名函数的形式。

let FunctionName = function*() { /* ... */ }

生成器函数的函数内容将会是对应生成器的运行内容,其中支持一种新的语法yield。它的作用与return有点相似,但并非退出函数,而是切出生成器运行时

你可以把整个生成器运行时看成一条长长的面条(while (true)则就是无限长的),JavaScript引擎在每一次遇到yield就要切一刀,而切面所成的“纹路”则是yield出来的值。

好吧这是瑞士卷

Generator

生(rui)成(shi)器(juan)在某种意义上可以看做为与JavaScript主线程分离的运行时(详细可参考我的另外一篇文章:http://lifemap.in/koa- co-and-coroutine/),它可以随时被 yield切回主线程(生成器不影响主线程)。

每一次生成器运行时被yield都可以带出一个值,使其回到主线程中;此后,也可以从主线程返回一个值回到生成器运行时中:

let inputValue = yield outputValue

生成器切出主线程并带出outputValue,主函数经过处理后(可以是异步的),把inputValue带回生成器中;主线程可以通过.next(inputValue)方法返回值到生成器运行时中。

基本使用方法

构建生成器函数

使用Generator 的第一步自然是要构建生成器函数,理清构建思路,比如我需要做一个生成斐波那契数列(俗称兔子数列)的生成器们则需要如何构建循环体呢?如果我需要在主线程不断获得结果,则需要在生成器中做无限循环,以保证其不断地生成。

而根据斐波那契数列的定义,第n (n ≥ 3) 项是第n - 1 项和第n - 2 之和,而第1 项和第2 项都是1。

function* fibo() {
  let [a, b] = [1, 1]

  yield a
  yield b

  while (true) {
    [a, b] = [b, a + b]
    yield b
  }
}

这样设计生成器函数,就可以先把预先设定好的首两项输出,然后通过无限循环不断把后一项输出。

启动生成器

生成器函数不能直接用来作为生成器使用,需要先使用这个函数得到一个生成器,用于运行生成器内容和接收返回值。

let gen = fibo()
运行生成器内容

得到生成器以后,我们就可以通过它进行数列项生成了。此处演示获得前10 项。

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 的运行原理

事实上,Generator的用法还是很多种,其中最为著名的一种便是使用Generator的特性模拟出ES7中的async/await特性。而其中最为著名的就是co和koa (基于co的Web Framework)了。详细可以看我的另外一篇文章:Koa, co and coroutine。

原生的模块化

在前文中,我提到了ES2015 在工程化方面上有着良好的优势,而采用的就是ES2015 中的原生模块化机制,足以证明它的重要性。

历史小回顾

在JavaScript 的发展历史上,曾出现过多种模块加载库,如RequireJS、SeaJS、FIS 等,而由它们衍生出来的JavaScript 模块化标准有CommonJS、AMD、CMD 和UMD 等。

其中最为典型的是Node.js 所遵循的CommonJS 和RequireJS 的AMD。

本文在此不再详细说明这些模块化方案,详细可以阅读What Is AMD, CommonJS, and UMD?

基本用法

正如前文所展示的使用方式一样,ES2015 中的模块化机制设计也是相当成熟的。基本上所有的CommonJS 或是AMD 代码都可以很快地转换为ES2015 标准的加载器代码。

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 中有很多种模块引入方式,我们可以根据实际需要选择一种使用。

全局引入

全局引入是最基本的引入方式,这跟CommonJS、AMD 等模块化标准并无两样,都是把目标模块的所有暴露的接口引入到一个命名空间中。

import name from 'module-name'
import * as name from 'module-name'

這跟 Node.js 所用的 CommonJS 類似:

var name = require('module-name')
局部引入

与CommonJS 等标准不同的是,ES2015 的模块引入机制支持引入模块的部份暴露接口,这在大型的组件开发中显得尤为方便,如React 的组件引入便是使用了该特性。

import { A, B, C } from 'module-name'

A()
B()
C()
接口暴露

ES2015 的接口暴露方式比CommonJS 等标准都要丰富和健壮,可见ECMA 委员会对这一部份的重视程度之高。

ES2015 的接口暴露有几种用法:

暴露单独接口
// module.js
export function method() { /* ... */ }

// main.js
import M from './module'
M.method()

基本的 `export` 語句可以調用多次,單獨使用可暴露一個對象到該模塊外。

###### 暴露覆蓋模塊

若需要實現像 CommonJS 中的 `module.exports = {}` 以覆蓋整個模塊的暴露對象,則需要在 `export` 語句后加上 `default`。

// module.js
export default {
  method1,
  method2
}

// main.js
import M from './module'
M.method1()

降级兼容

在实际应用中,我们暂时还需要使用babel 等工具对代码进行降级​​兼容。庆幸的是,babel 支持CommonJS、AMD、UMD 等模块化标准的降级兼容,我们可以根据项目的实际情况选择降级目标。

$ npm i babel-preset-es2015
$ babel --presets es2015 -m common -d dist/common/ src/
$ babel --presets es2015 -m amd -d dist/amd/ src/
$ babel --presets es2015 -m umd -d dist/umd/ src/

Promise

Promise,作为一个老生常谈的话题,早已被聪明的工程师们“玩坏”了。

光是Promise自身,目前就有多种标准,而目前最为流行的是Promises/A+。而ES2015中的Promise便是基于Promises/A+制定的。

概念

Promise 是一种用于解决回调函数无限嵌套的工具(当然,这只是其中一种),其字面意义为“保证”。它的作用便是“免去”异步操作的回调函数,保证能通过后续监听而得到返回值,或对错误处理。它能使异步操作变得井然有序,也更好控制。我们以在浏览器中访问一个API,解析返回的JSON 数据。

fetch('http://example/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 在设计上具有原子性,即只有两种状态:未开始和结束(无论成功与否都算是结束),这让我们在调用支持Promise 的异步方法时,逻辑将变得非常简单,这在大规模的软件工程开发中具有良好的健壮性。

基本用法

创建Promise 对象

要为一个函数赋予Promise的能力,先要创建一个Promise对象,并将其作为函数值返回。Promise构造函数要求传入一个函数,并带有resolvereject参数。这是两个用于结束Promise等待的函数,对应的成功失败。而我们的逻辑代码就在这个函数中进行。

此处,因为必须要让这个函数包裹逻辑代码,所以如果需要用到this时,则需要使用箭头函数或者在前面做一个this的别名。

function fetchData() {
  return new Promise((resolve, reject) => {
    // ...
  })
}
进行异步操作

事实上,在异步操作内,并不需要对Promise 对象进行操作(除非有特殊需求)。

function fetchData() {
  return new Promise((resolve, reject) => {
    api.call('fetch_data', (err, data) => {
      if (err) return reject(err)

      resolve(data)
    })
  })
}

因为在Promise定义的过程中,也会出现数层回调嵌套的情况,如果需要使用this的话,便显现出了箭头函数的优势了。

使用Promise

让异步操作函数支持Promise 后,我们就可以享受Promise 带来的优雅和便捷了~

fetchData()
  .then(data => {
    // ...

    return storeInFileSystem(data)
  })
  .then(data => {
    return renderUIAnimated(data)
  })
  .catch(err => console.error(err))

弊端

虽说Promise 确实很优雅,但是这是在所有需要用到的异步方法都支持Promise 且遵循标准。而且链式Promise 强制性要求逻辑必须是线性单向的,一旦出现如并行、回溯等情况,Promise 便显得十分累赘。

所以在目前的最佳实践中,Promise 会作为一种接口定义方法,而不是逻辑处理工具。后文将会详细阐述这种最佳实践。

Symbol

Symbol 是一种很有意思的概念,它跟Swift 中的Selector 有点相像,但也更特别。在JavaScript 中,对象的属性名称可以是字符串或数字。而如今又多了一个Symbol。那Symbol 究竟有什么用?

首先,我们要了解的是,Symbol 对象是具有唯一性的,也就是说,每一个Symbol 对象都是唯一的,即便我们看不到它的区别在哪里。这就意味着,我们可以用它来保证一​​些数据的安全性。

console.log(Symbol('key') == Symbol('key')) //=> false

如果将一个Symbol 隐藏于一个封闭的作用域内,并作为一个对象中某属性的键,则外层作用域中便无法取得该属性的值,有效保障了某些私有库的代码安全性。

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 除了带给我们数据安全性以外,还带来了一些很神奇的黑科技,简直了。

Symbol.iterator

除Symbol以外,ES2015还为JavaScript带来了for...of语句,这个跟原本的for...in又有什么区别?

我们还是以前面的斐波那契数列作为例子。Iterator 在Java 中经常用到中会经常用到,意为“迭代器”,你可以把它理解为用于循环的工具。

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!看到这个for…of是否有种兴奋的感觉?虽然说创建fibo的时候稍微有点麻烦……

不如我们先来看看这个fibo究竟是怎么定义出来了。首先,我们要了解到JavaScript引擎(或编译器)在处理for...of的时候,会从of后的对象取得Symbol.iterator这属性键的值,为一个函数。它要求要返回一个包含next方法的对象,用于不断迭代。而因为Symbol.iterator所在键值对的值是一个函数,这就让我们有了自由发挥的空间,比如定义局部变量等等。

每当for...of进行了一次循环,都会执行一次该对象的next方法,已得到下一个值,并检查是否迭代完成。随着ES7的开发,for...of所能发挥的潜能将会越来越强。

还有更多的Symbol黑科技等待挖掘,再次本文不作详细阐述,如有兴趣,可以看看Mozilla Developer Center上的介绍。

Proxy(代理)

Proxy 是ECMAScript 中的一种新概念,它有很多好玩的用途,从基本的作用说就是:Proxy 可以在不入侵目标对象的情况下,对逻辑行为进行拦截和处理。

比如说我想记录下我代码中某些接口的使用情况,以供数据分析所用,但是因为目标代码中是严格控制的,所以不能对其进行修改,而另外写一个对象来对目标对象做代理也很麻烦。那么Proxy 便可以提供一种比较简单的方法来实现这一需求。

假设我要对api这一对象进行拦截并记录下代码行为,我就可以这样做:

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 的兼容性很差,哪怕是降级兼容也难以实现。


到这里,相信你已经对ES2015 中的大部份新特性有所了解了。那么现在,就结合我们原有的JavaScript 技能,开始使用ES2015 构建一个具有工程化特点的项目吧。

ES2015 的前端开发实战

事实上,你们都应该有听说过React 这个来自Facebook 的前端框架,因为现在它实在太火了。React 与ES2015 的关系可谓深厚,React 在开发上便要求使用ES2015 标准,因其DSL ──JSX的存在,所以必须要依赖Babel将其编译成JavaScript。

但同样是由于JSX 的存在,本文章并不会采用React 作为前端框架,以避免读者对JSX 和HTML 的误解。我们会采用同样优秀的前端MVVM 框架── Vue 进行开发。

数据部份,将会使用MinDB 进行存储和处理。MinDB 是由笔者开发的一个用于JavaScript 环境的简易而健壮的数据库,它默认使用DOM Storage 作为其存储容器,在其他环境中可以通过更换Store Interface 以兼容绝大部份JavaScript 运行环境。

Vue.js的使用教程可以参考Vue.js的官方教程。

构建界面

我们首先简单地用LayoutIt 搭建一个用Bootstrap 构架的页面,其中包含了DEMO 的首页和文章内容页,此后我们将会使用这个模板搭建我们的JavaScript 代码架构。

接下来,我们需要通过对页面的功能块进行组件划分,以便于使用组件化的架构搭建前端页面。

我们可以大致分为Index、Post 和Publish 三个页面,也可以说三个路由方向;而我们还可以把页面中的组块分为:文章列表、文章、侧边栏、评论框等。

以此,我们可以设计出以下结构,以作为这个项目的组织结构:

Routes       Components
 |- Index ----|- Posts
 |            |- Sidebar
 |
 |- Post -----|- Post
 |            |- Comments
 |
 |- Publish

首页包含了文章列表、侧边栏两个组件;文章页面包含文章内容组件和评论框组件(此处我们使用多说评论框作为我们的组件);而文章发布页则可以单独为一个路由器,而不需要分出组件。

代码结构定义

因我们是以babel进行ES2015降级兼容的,所以我们最好可以采用分离的结构,这里我们使用srcdist

我们此处以比较简单的结构构建我们的DEMO:

app
 |- src                      程序的源文件目錄
 |   |- controllers          後端的路由處理器
 |   |- lib                  後端需要引用的一些庫
 |   |- public               前端 JavaScript 源文件
 |   |   |- controllers      前端的路由處理器
 |   |   |- components       前端組件
 |   |   |- models           前端數據層
 |   |   |- config.js        前端的配置文件
 |   |   |- main.js          前端 JavaScript 入口
 |   |- app.js               後端程序入口
 |   |- routes.js            後端路由表
 |- dist                     降級兼容輸出目錄
 |   |- public
 |       |- css
 |       |- index.html       前端 HTML 入口
 |- gulpfile.js              Gulp 構建配置文件
 |- package.json             Node.js 項目配置文件

而我们在这一章节中则专注于public这一目录即可,Node.js部份将在下一章节详细展示。

架构设计

模块化

因为有了ES2015 自身的模块化机制,我们就不必使用RequireJS 等模块加载库了,通过Browserify 我们我可以将整个前端的JavaScript 程序打包到一个.js 文件中,而这一步骤我们使用Gulp 来完成。

详细的Gulp使用教程可以参考:了不起的任务运行器Gulp基础教程

数据支持

我们的数据将从后端的Node.js 程序中获取,以API 的形式获得。

另外,为了得到更佳的用户体验,我们将使用MinDB 作为这个DEMO 的前端数据库,减少网络请求次数,优化使用体验。

界面渲染

为了能让我们的界面定义能够足够简单,我们在这使用了Vue 作为前端开发框架,将其与MinDB 对接,负责渲染我们的页面。

其中,我们会利用Vue 的组件系统来实现我们制定下的组件设计。另外,我们还会使用watchman.js 来实现前端的路由器,以便我们对不同的页面的逻辑进行分离。

构建应用

在开始编写业务代码之前,我们需要先安装好我们所需要的依赖库。

$ 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()

在入口中,我们将做如下任务:

  1. 引入所有的路由相应器
  2. 将路由相应器通过watchman.js 绑定到对应的url 规则中
  3. 建立在共用模板中存在的需要实例化的组件

因为我们还么有开始动工路由相应器,所以我们先把“建立在共用模板中存在的需要实例化的组件”这一任务完成。

在共用模板中,有唯一一个必须的共用组件就是页面切换器── 一个用于包含所有的页面的元素。

<div class="row" id="wrapper" v-html="html"></div>

对应的,我们将在入口文件中使用Vue 建立其对应的ViewModel,并将其绑定至DOM 元素。

let layoutVM = new Vue({
  el: '#wrapper',
  data: {
    html: ''
  }
})

以后可以通过改变layoutVM.$data.html来改变#wrapper的内容,配合watchman.js以加载不同的页面内容。

为了能在路由相应器中改变layoutVM的参数,我们将其作为watchman.js给予相应器的上下文参数中的一个属性。

// ...
  .use((ctx, next) => {
    ctx.query = qs.parse(window.location.search.substr(1))
    ctx.layoutVM = layoutVM
    next()
  })

数据层:文章

我们单独把文章的查询、读取和创建等操作抽象成一个库,使其与逻辑层分离,让代码更美观。

在此之前,我们先定义好从后端用于取得文章数据的API:

  1. URL: /api/posts/list
  2. 参数: 
    • page 当前页数,每页10 条记录

我们可以直接用新的Ajax API 来进行API 访问。

import 'whatwg-fetch'
import min from 'min'

async function listPosts(page = 0) {
  const count = 10

  // 檢查 MinDB 是否存在數據
  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: postments.length,
          get summary() {
            return post.content.substr(0, 20) + '...'
          }
        }
      })

    // 將數據存儲到 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 讀取數據
    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)会将文章的ID存入MinDB中名为posts:id的集中,这个集用于保存所有文章的ID;``min.hmset(post:${post.id}`, post)完成对首页的所有文章列表支持后,我们还需要简单的定义一个用于读取单篇文章数据的API。

如果用户是从首页的文章列表进来的,那么我们就可以直接从MinDB 中读取文章数据。但如果用户是直接通过url 打开网址的话,MinDB 有可能并没有存储文章数据,那么我们就通过API 从后端获取数据,并存储在MinDB 中。

  • URL:/api/posts/:id

    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: postments.length,
      get summary() {
        return post.content.substr(0, 20) + '...'
      }
    })
    
    
    return post
    

    } }

完成用于读取的接口后,我们也该做做用于写入的接口了。同样的,我们也先来把API 定义一下。

  1. URL:/api/posts/new
  2. Body(JSON): 
    • title
    • content
    • author

我们也可以通过fetch来发出POST请求。

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
}

最后我们就可以暴露出这几个接口了。

export default {
  listPosts,
  getPost,
  publishPost
}

路由:首页

首先我们确定一下首页中我们需要做些什么:

  1. 改变layoutVM的HTML数据,为后面的页面渲染做准备
  2. 分别从后端加载文章数据和多说加载评论数,加载完以后存入MinDB 中
  3. 从MinDB 中加载已缓存的数据
  4. 建立对应的VM,并传入数据
准备页面渲染

首先,我们需要为watchman.js的路由提供一个函数以作相应器,并包含一个为context的参数。我们则可以将其作为模块的暴露值。

export default function(ctx) {
  // 改變 layoutVM
  ctx.layoutVM.$data.html = `
    <h1>
      ES2015 实战 - 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="侧边栏" content="{{content}}" list="{{list}}"></panel>
    </div>
  `
}

此处将首页的HTML结构赋予layoutVM,并让其渲染至页面中。

加载数据

因为我们之前已经将数据层抽象化了,所以我们此处只需通过我们的抽象数据层读取我们所需要的数据即可。

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)
}
设计组件

在首页的HTML中,我们用到了两个Component,分别为post-in-listpanel,我们分别在components文件夹中分别建立post-in-list.jspanel.js。我们从我们之前通过LayoutIt简单建立的HTML模板中,抽出对应的部份,并将其作为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}} 发表</small> | <a class="btn" href="/#!/post/{{id}}">查看更多 »</a>
    </p>
  </div>
`

我们可以通过Vue 的双向绑定机制将数据插入到模板中。

Vueponent('post-in-list', {
  template: template,
  replace: true
})

根据Vue 的组件机制,我们可以通过对组件标签中加入自定义属性来传入参数,以供组件中使用。

但此处我们先用Vue的v-repeat指令来进行循环使用组件。

<post-in-list v-repeat="posts"></post-in-list>

panel.js 同理建立。

路由:文章页面

相比首页,文章页面要简单得多。因为我们在首页已经将数据加载到MinDB 中了,所以我们可以直接从MinDB 中读取数据,然后将其渲染到页面中。

// ...
let post = await min.hgetall(`post:${this.id}`)
// ...

然后,我们再从之前设计好的页面模板中,抽出我们需要用来作为文章页面的内容页。

<h1 v-text="title"></h1> 
<small>由 {{author}} 发表</small>
<div class="post" v-html="content | marked"></div>

我们同样是同样通过对layoutVM的操作,来准备页面的渲染。在完成渲染准备后,我们就可以开始获取数据了。

let post = await min.hgetall(`post:${this.id}`)

在获得相应的文章数据以后,我们就可以通过建立一个组件来将其渲染至页面中。其中,要注意的是我们需要通过Vue 的一些API 来整合数据、渲染等步骤。

在这我不再详细说明其构建步骤,与上一小节相同。

import Vue from 'vue'
import min from 'min'
import marked from 'marked'

const template = `
  <h1 v-text="title"></h1>
  <small>由 {{author}} 发表</small>
  <div class="post" v-html="content | marked"></div>
`

let postVm = Vueponent('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,这是一种用于对异步操作进行“打扁”的特性,它可以把异步操作以同步的语法编写。如上文所说,在ES2015中,我们可以用co来模拟async/await特性。

路由:发布新文章

在发布新文章的页面中,我们直接调用我们之前建立好的数据抽象层的接口,将新数据传向后端,并保存在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
  }
})

路由绑定

在完成路由响应器的开发后,我们就可以把他们都绑定到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
})

// ...

这样,就可以让我们之前的路由结构都绑定到入口文件中:

  1. 首页绑定到/#!/
  2. 文章页面则绑定到了#!/post/:id,比如#!/post/123则表示id为123的文章页面
  3. #!/new 则绑定了新建文章的页面

合并代码

在完成三个我们所构建的路由设计后,我们就可以用Browserify 把我们的代码打包到一个文件中,以作为整个项目的入口文件。此处,我们再引入Gulp 作为我们的构建辅助器,而不需要直接使用Browserify 的命令行进行构建。

在开始编写Gulpfile 之前,我们先安装我们所需要的依赖库:

$ npm install gulp browserify babelify vinyl-source-stream vinyl-buffer babel-preset-es2015-without-regenerator babel-plugin-transform-async-to-generator --save

将依赖库安装好以后,我们就可以开始编写Gulp 的配置文件了。

在本文将近完成的时候,Babel 发布了版本6,其API 与5 版本有着相当大的区别,且Babel 6 并不向前兼容,详细更改此处不作介绍。

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' ])

在这个配置文件中,我们把前端的代码中的入口文件传入babel 中,然后将其打包成bundle.js。

最后,我们可以在我们最开始通过Layoutit 所设计的页面中,把可以用于包含替换内容的部份去掉,然后引入我们通过Browserify 生成的bundle.js。

完成JavaScript 部份的开发后,我们再将所需要的静态资源文件加载到HTML 中,我们就可以看到这个基本脱离后端的前端Web App 的效果了。


ES2015 的Node.js 开发实战

就目前来说,能最痛快地使用ES2015 中各种新特性进行JavaScript 开发的环境,无疑就是Node.js。就Node.js 本身来说,就跟前端的JavaScript 环境有着本质上的区别,Node.js 有着完整意义上的异步IO 机制, 有着无穷无尽的应用领域,而且在语法角度上遇到问题机率比在前端大不少。甚至可以说,Node.js 一直是等着ES2015 的到来的,Node.js 加上ES2015 简直就是如虎添翼了。

从V8 引擎开始实验性的开始兼容ES6 代码时,Node.js 便开始马上跟进,在Node.js 中开放ES6 的兼容选项,如Generator、Classes 等等。经过相当长一段时间的测试和磨合后,就以在Node.js 上使用ES6 标准进行应用开发这件事来说,已经变得越来越成熟,越来越多的开发者走上了ES6 这条“不归路”。

一些针对Node.js + ES6 的开发模式和第三方库也如雨后春笋般冒出,其中最为人所熟知的便是以co 为基础所建立的Web 框架Koa。Koa 由TJ 等express 原班人马打造,目前也有越来越多的中国开发者加入到Koa 的开发团队中来,为前沿Node.js 的开发做贡献。

co通过使用ES2015中的Generator特性来模拟ES7中相当诱人的async/await特性,可以让复杂的异步方法调用及处理变得像同步操作一样简单。引用响马大叔在他所维护的某项目中的一句话:

用同步代码抒发异步情怀

在本章节中,我将以一个简单的后端架构体系,来介绍ES2015 在Node.js 开发中的基于Koa 的一种优秀实践方式。

因为Node.js 自带模块机制,所以用babel 对ES2015 的模块语法做降级兼容的时候,只需降至Node.js 所使用的CommonJS 标准即可。

不一样的是,ES2015的模块语法是一种声明式语法,根据ES2015中的规定,模块引入和暴露都需要在当前文件中的最顶层,而不能像CommonJS中的require()那样可以在任何地方使用;使用import引入的模块所在命名空间将会是一个常量,在babel的降级兼容中会进行代码检查,保证模块命名空间的安全性。

架构设计

因为我们这个DEMO的数据结构并不复杂,所以我们可以直接使用MongoDB作为我们的后端数据库,用于存储我们的文章数据。我们可以通过monk库作为我们读取、操作MongoDB的客户端库。在架构上,Koa将作为Web开发框架,配合co等库实现全“同步”的代码编写方式。

构建应用

这就让我们一步一步来吧,创建Node.js 应用并安装依赖。

$ npm init
$ npm install koa koa-middlewares koa-static monk co-monk thunkify --save

在上一个章节中,我们已经建立了整个DEMO 的文件结构,在此我们再展示一遍:

app
 |- src                      程序的源文件目錄
 |   |- controllers          後端的路由處理器
 |   |- models               數據抽象層
 |   |- lib                  後端需要引用的一些庫
 |   |- public               前端 JavaScript 源文件
 |   |- app.js               後端程序入口
 |   |- routes.js            後端路由表
 |- dist                     降級兼容輸出目錄
 |- gulpfile.js              Gulp 構建配置文件
 |- package.json             Node.js 項目配置文件

src/controllers中,包含我们用来相应请求的控制器文件;src/lib文件夹则包含了我们需要的一些抽象库;src/public文件则包含了在上一章节中我们建立的前端应用程序;src/app.js是该应用入口文件的源文件;src/routes.js则是应用的路由表文件。

入口文件

在入口文件中,我们需要完成几件事情:

  1. 创建Koa 应用,并监听指定端口
  2. 将所需要的Koa 中间件接入我们所创建的Koa 应用中,如静态资源服务
  3. 引入路由表文件,将路由控制器接入我们所建立的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}`)
})

数据抽象层

为了方便我们在co的环境中使用MongoDB,我们选择了monkco-monk两个库进行组合,并抽象出链接库。

// 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 實例
 * 
 * @param  {String} name collection name
 * @return {Object}      Collection
 */
function collection(name) {
  return wrap(db.get(name))
}

export default {
  collection
}

通过这个抽象库,我们就可以避免每次获取MongoDB 中的Collection 实例时都需要连接一遍数据库了。

// models/posts.js
import mongo from '../lib/mongo'

export default mongo.collection('posts')

#### Posts 控制器

完成了數據層的抽象處理后,我們就可以將其用於我們的控制器了,參考 `monk` 的文檔,我們可以對這個 Posts Collection 進行我們所需要的操作。

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))
})

此处我们使用thunkifyrequest做了一点小小的封装工作,而因为request库自身的callback并不是标准的callback形式,所以我们并不能直接把request函数传入thunkify中,我们需要的是callback中的第三个参数body,所以我们需要自行包装一层函数以取得body并返回到co中。

API:获取所有文章

我们可以通过这个API 获取存储在MongoDB 中的所有文章,并支持分页。支持提供当前获取的页数,每页10篇文章,提供每篇文章的标题、作者、文章内容和评论。

// 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
  })

  // 從多說獲取評論
  posts = yield posts.map(post => {
    return function*() {
      let duoshuoReply = JSON.parse(yield requestAsync(`http://api.duoshuo/threads/listPosts.json?short_name=es2015-in-action&thread_key=${post._id}&page=0&limit=1000`))

      var commentsId = Object.keys(duoshuoReply.parentPosts)
      postments = commentsId.map(id => duoshuoReply.parentPosts[id])

      return post
    }
  })

  // 返回結果
  this.body = {
    posts: posts
  }
}

此处我们用到了co的一个很有意思的特性,并行处理异步操作。我们通过对从MongoDB中取得数据进行#map()方法的操作,返回一组Generator Functions,并将这个数组传给yield,co便可以将这些Generator Functions全部执行,并统一返回结果。同样的我们还可以使用对象来进行类似的操作:

co(function*() {
  let result = yield {
    posts: getPostsAsync(),
    hot: getHotPosts(),
    latestComments: getComments(10)
  }

  result //=> { posts: [...], hot: [...], latestComments: [...] }
})
API:获取指定文章

这个API 用于通过提供指定文章的ID,返回文章的数据。

// 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.duoshuo/threads/listPosts.json?short_name=es2015-in-action&thread_key=${id}&page=0&limit=1000`))

  var commentsId = Object.keys(duoshuoReply.parentPosts)
  postments = commentsId.map(id => duoshuoReply.parentPosts[id])

  this.body = {
    post: post
  }
}
API:发布新文章

相比上面两个API,发布新文章的API 在逻辑上则要简单得多,只需要向Collection 内插入新元素,然后将得到的文档返回至客户端既可以。

// 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 以外,我们还需要提供文章评论的API,以方便日后该DEMO 向移动端扩展和弥补多说评论框在原生移动端上的不足。由于评论的数据并不是存储在项目数据库当中,所以我们也不需要为它创建一个数据抽象层文件,而是直接从控制器入手。

API:获取指定文章的评论
// GET /api/comments/post/:id
router.get.fetchCommentsInPost = function*() {
  let postId = this.params.id

  let duoshuoReply = JSON.parse(yield requestAsync(`http://api.duoshuo/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:发表新评论

同样是为了扩展系统的API,我们通过多说的API,允许使用API​​ 来向文章发表评论。

// 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.duoshuo/posts/create.json`,
    json: true,

    body: {
      short_name: duoshuo.short_name,
      secret: duoshuo.secret,
      thread_key: postId,
      message: message
    }
  })

  this.body = {
    comment: reply.response
  }
}

配置路由

完成控制器的开发后,我们是时候把路由器跟控制器都连接起来了,我们在src/routes.js中会将所有控制器都绑定到路由上,成为一个类似于路由表的文件。

import { router as Router } from 'koa-middlewares'

首先,我们要将所有的控制器引入到路由文件中来。

import posts from './controllers/posts'
import comments from './controllers/comments'

然后,创建一个路由器实例,并将所有控制器的响应器和URL 规则一一绑定。

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

配置任务文件

经过对数据抽象层、逻辑控制器、路由器的开发后,我们便可以将所有代码利用Gulp 进行代码构建了。

我们先安装好所需要的依赖库。

$ npm install gulp gulp-babel vinyl-buffer vinyl-source-stream babelify browserify --save-dev
$ touch gulpfile.js

很可惜的是,Gulp 原生并不支持ES2015 标准的代码,所以在此我们也只能通过ES5 标准的代码编写任务文件了。

'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')

我们主要需要完成两个构建任务:

  1. 编译并构建前端JavaScript 文件
  2. 编译后端JavaScript 文件

在构建前端JavaScript 文件的过程中,我们需要利用Browserify 配合babelify 进行代码编译和合并。

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'))
})

我们将代码编译好以后,便将其复制到dist/public文件夹中,这也是我们Node.js后端处理静态资源请求的响应地址。

而构建后端代码则更为简单,因为我们只需要将其编译并复制到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 文件的编写以后,我们就可以进行代码构建了。

$ 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文件夹中已经编译好的代码运行起来了。

$ node dist/app.js
Demo is running, port:3000

不出意外,我们就可以看到我们想要的效果了。


前方高能反应

部署到DaoCloud

完成了代码的开发,我们还需要将我们的项目部署到线上,让别人看到我们的成果~

Docker 是目前最流行的一种容器化应用搭建工具,我们可以通过Docker 快速地将我们的应用部署在任何支持Docker 的地方,哪怕是Raspberry Pi 还是公司的主机上。

而DaoCloud 则是目前国内做Docker 商业化体验最好的公司,他们提供了一系列帮助开发者和企业快速使用Docker 进行项目部署的工具。这里我们将介绍如何将我们的DEMO 部署到DaoCloud 的简易容器中。

Dockerfile

在使用Docker 进行项目部署前,我们需要在项目中新建一个Dockerfile 以表达我们的镜像构建任务。

因为我们用的是Node.js 作为项目基础运行环境,所以我们需要从Node.js 的官方Docker 镜像中引用过来。

FROM node:onbuild

因为我们已经在package.json中写好了对gulp的依赖,这样我们就可以直接在docker build的时候对代码进行编译。

RUN ./node_modules/.bin/gulp

此外,我们还需要安装另外一个依赖库pm2,我们需要使用pm2作为我们项目的守护程序。

$ npm install pm2 --save-dev

然后,我们简单地向Docker的宿主机器申请暴露80端口,并利用pm2启动Node.js程序。

EXPOSE 80

CMD ./node_modules/.bin/pm2 start dist/app.js --name ES2015-In-Action --no-daemon

至此,我们已经完成了Dockerfile 的编写,接下来就可以将项目代码上传到GitHub 等地方,供DaoCloud 使用了。这里我不再说明Git 和GitHub 的使用。

创建DaoCloud 上的MongoDB 服务

借助于Docker 的强大扩容性,我们可以在DaoCloud 上很方便地创建用于项目的MongoDB 服务。

在“服务集成”标签页中,我们可以选择部署一个MongoDB 服务。

创建好MongoDB 服务后,我们就要将我们上传到GitHub 的项目代码利用DaoCloud 进行镜像构建了。

代码构建

DaoCloud 提供了一个十分方便的工具,可以把我们存储在GitHub、BitBucket、GitCafe、Coding 等地方的项目代码,通过其中的Dockerfile 构建成一个Docker 镜像,用于部署到管理在DaoCloud 上的容器。

通过绑定GitHub 账号,我们可以选择之前发布在GitHub 上的项目,然后拉取到DaoCloud 中。

构建完成以后,我们就可以在“镜像仓库”中看到我们的项目镜像了。

我们将其部署到一个DaoCloud 的容器中,并且把它与之前创建的MongoDB 服务绑定。

最后的最后,我们点击“立即部署”,等待部署成功就可以看到我们的项目线上状态了。

我们可以在这里看到我部署的DEMO:http://es2015-demo.daoapp.io/

而我们的DEMO,可以在这里查看详细代码:https://github/iwillwen/es2015-demo

至此,我们已经完成了Node.js 端和前端的构建,并且将其部署到了DaoCloud 上,以供浏览。下面,我们再来看看,正在发展中的ES7 又能给我们带来什么惊喜吧。


一窥ES7

async/await

上文中我们提及到co是一个利用Generator模拟ES7中async/await特性的工具,那么,这个async/await究竟又是什么呢?它跟co又有什么区别呢?

我们知道,Generator Function 与普通的Function 在执行方式上有着本质的区别,在某种意义上是无法共同使用的。但是,对于ES7 的Async Function 来说,这一点并不存在!它可以以普通函数的执行方式使用,并且有着Generator Function 的异步优越性,它甚至可以作为事件响应函数使用。

async function fetchData() {
  let res = await fetch('/api/fetch/data')
  let reply = await res.json()

  return reply
}

var reply = fetchData() //=> DATA...

遗憾的是,async/await所支持的并不如co多,如并行执行等都暂时没有得到支持。

Decorators

对于JavaScript 开发者来说,Decorators 又是一种新的概念,不过它在Python 等语言中早已被玩出各种花式。

Decorator 的定义如下:

  1. 是一个表达式
  2. Decorator 会调用一个对应的函数
  3. 调用的函数中可以包含target(装饰的目标对象)、name(装饰目标的名称)和descriptor(描述器)三个参数
  4. 调用的函数可以返回一个新的描述器以应用到装饰目标对象上

PS:如果你不记得descriptor是什么的话,请回顾一下方法。Object.defineProperty()

简单实例

我们在实现一个类的时候,有的属性并不想被for..inObject.keys()等方法检索到,那么在ES5时代,我们会用到Object.defineProperty()方法来实现:

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 来很简单地实现这个需求:

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 的支持当然不会就此收手,就让我们看看我们还能用Decorator 做点什么神奇的事情。

假如我们要实现一个类似于Koa 和PHP 中的CI 的框架,且利用Decorator 特性实现URL 路由,我们可以这样做。

// 框架內部
// 控制器
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)
  }
}

// 路由執行部份
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)
}

最重要的是,同一个修饰对象是可以同时使用多个修饰器的,所以说我们还可以用修饰器实现很多很多有意思的功能。


后记

对于一个普通的JavaScript 开发者来说,ES2015 可能会让人觉得很模糊和难以学习,因为ES2015 中带来了许多我们从前没有在JavaScript 中接触过的概念和特性。但是经过长时间的考察,我们不难发现ES2015 始终是JavaScript 的发展方向,这是不可避免的。因此我要在很长一段时间内都向身边的或是社区中的JavaScript 开发者推广ES2015,推荐他们使用最新的技术。

这篇文章说长不长,说短也不短,我只能在有限的文字篇幅内尽可能把更多的知识展示出来,更深入的细节还需要读者自行探索。无论如何,若是这篇文章能引起各位JavaScript 开发者对ES2015 的兴趣和重视,并且中从学会了如何在项目中使用ES2015 标准进行开发,那么这篇文章的目的就已经达到了。

更多推荐

给JavaScript 初心者的ES2015 实战