ES6和ES7全面入门


ES2015(ES6)和ES2016(ES7)的知识总结

1.前言

1.1 介绍

本篇博客的大部分内容和案例均来源于互联网,因为自己也算不上权威就暂时没有这个必要

1.2 参考资料

2.ES6

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

ECMAScript 和 JavaScript 的关系

一个常见的问题是,ECMAScript 和 JavaScript 到底是什么关系?

要讲清楚这个问题,需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。

该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。

因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 Jscript 和 ActionScript)。日常场合,这两个词是可以互换的。

let、const

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

但由于在 ES6 之前的 ECMAScript 标准中,并没有原生的实现,所以在降级编译中,会马上进行引用检查,然后使用 var 代替。

1
2
3
4
// foo.js
const foo = 'bar'

foo = 'newvalue'

块级作用域

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

JavaScript 没有块级作用域

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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 中,我们只需做出一个小小的改动,便可以解决该问题(假设所使用的浏览器已经支持所需要的特性):

1
2
3
4
5
// ...
for (/* var */ let i = 0; i < buttons.length; i++) {
// ...
}
// ...

通过把 for 语句中对计数器 i 的定义语句从 var 换成 let,即可。因为 let 语句会使该变量处于一个块级作用域中,从而让事件监听回调函数中的变量引用得到保持。我们不妨看看改进后的代码经过 babel 的编译会变成什么样子:

1
2
3
4
5
6
7
8
9
10
11
// ...
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)

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

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

使用

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

1
2
3
4
5
6
7
8
9
10
11
foo => foo + ' world' // means return `foo + ' world'`

(foo, bar) => foo + bar

foo => {
return foo + ' world'
}

(foo, bar) => {
return foo + bar
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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' ]

箭头函数与上下文绑定

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

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {
hello: 'world',
foo() {
let bar = () => {
return this.hello
}
return bar
}
}

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

上面代码中的 obj.foo 等价于:

1
2
3
4
5
6
7
8
9
// ...
foo() {
let bar = (function() {
return this.hello
}).bind(this)

return bar
}
// ...

注意事项

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

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

1
2
3
4
5
6
7
8
9
10
11
12
// 假设当前运行环境为浏览器,故顶层作上下文为 `window`
let obj = {
msg: 'pong',

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

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

模板字符串

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

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

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

支持变量注入(useful)

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

1
2
3
4
5
6
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

支持换行

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

1
2
3
4
5
let sql = `
SELECT * FROM Users
WHERE FirstName='Mike'
LIMIT 5;
`

对象字面量扩展语法

(本质上还是语法糖,但是挺爽的)

方法属性省略 function (useful)

1
2
3
4
5
6
7
8
9
10
11
let obj = {
// before
foo: function() {
return 'foo'
},

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

同名方法属性省略语法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// module.js
export default {
someMethod
}

function someMethod() {
// ...
}

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

Module.someMethod()

可以动态计算的属性名称

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 }
]

表达式解构(useful)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Matching with object
function search(query) {
/* 交换变量 */
[x, y] = [y, x];

/* 从函数返回多个值 */
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();

/* 函数参数的定义 */
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
}

/* 提取json数据 */
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};

let { id, status, data: number } = jsonData;

/* 函数参数的默认 */
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};

/* 导入模块 */
const { SourceMapConsumer, SourceNode } = require("source-map");

函数参数表达、传参

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

默认参数值

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

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

后续参数

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

1
2
3
4
5
6
7
8
9
10
function fetchSomethings() {
var args = [].slice.apply(arguments)

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

// ...
}

而在 ES2015 中,我们可以很简单的使用 … 语法糖来实现:

1
2
3
4
5
6
function fetchSomethings(...args) {
// ...
}
function doSomeOthers(name, ...args) {
// ...
}

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

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

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

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

解构传参

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

1
2
3
4
5
6
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)。它们都具有元素唯一性,若添加了已存在的元素,会被自动忽略。

1
2
3
4
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 对元素有严格要求,必须是 Object,当然了,你也可以用 new String(‘…’) 等形式处理元素。

1
2
3
4
5
6
7
8
9
10
11
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,这可以实现从前不能实现或难以实现的功能,如在项目逻辑层实现数据索引等。

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

1
2
3
4
5
6
7
8
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 中的标准类机制。为什么是带有双引号的呢?因为我们不难发现这样一个现象:

1
2
3
$ node
> class Foo {}
[Function: Foo]

回想一下在 ES2015 以前的时代中,我们是怎么在 JavaScript 中实现类的?
function Foo() {} var foo = new Foo()

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

遗憾与期望

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

  1. 不支持私有属性(private)
  2. 不支持前置属性定义,但可用 get 语句和 set 语句实现
  3. 不支持多重继承

没有类似于协议(Protocl)或接口(Interface)等的概念
中肯地说,ES2015 的类机制依然有待加强。但总的来说,是值得尝试和讨论的,我们可以像从前一样,不断尝试新的方法,促进 ECMAScript 标准的发展。

Promise

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

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

概念

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

1
2
3
4
5
6
7
fetch('http://example.com/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 构造函数要求传入一个函数,并带有 resolve 和 reject 参数。这是两个用于结束 Promise 等待的函数,对应的成功和失败。而我们的逻辑代码就在这个函数中进行。

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

1
2
3
4
5
function fetchData() {
return new Promise((resolve, reject) => {
// ...
})
}

进行异步操作

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

1
2
3
4
5
6
7
8
9
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 带来的优雅和便捷了~

1
2
3
4
5
6
7
8
9
10
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 隐藏于一个封闭的作用域内,并作为一个对象中某属性的键,则外层作用域中便无法取得该属性的值,有效保障了某些私有库的代码安全性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

Proxy(代理)

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

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

注意,要使得代理起作用,必须对proxy对象操作而不是原对象

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 => {
// ...
})

3.ES7

async/await

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

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

1
2
3
4
5
6
7
8
async function fetchData() {
let res = await fetch('/api/fetch/data')
let reply = await res.json()

return reply
}

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

Decorators

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

Decorator 的定义如下:

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

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

简单实例

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 来很简单地实现这个需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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' ]

修饰器

修饰器是一个对类进行处理的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

黑科技

正如上面所说,Decorator 在编程中早已不是什么新东西,特别是在 Python 中早已被玩出各种花样。聪明的工程师们看到 ES7 的支持当然不会就此收手,就让我们看看我们还能用 Decorator 做点什么神奇的事情。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 框架内部
// 控制器
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)
}

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

编程风格

let取代var

let和var作用相似,而且let没有副作用

全局常量和线程安全

在let和const之间,建议优先选择const,尤其是在全局环境,不应该设置变量,只应该设置常量

const优于let有几个原因。一个是const可以提醒阅读程序的人,这个变量不应该改变;另一个是const比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对const进行优化,所以多使用const,有利于提高程序的运行效率,也就是说let和const的本质区别,其实是编译器内部的处理不同。

const声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。
所有的函数都应该设置为常量。

长远来看,JavaScript 可能会有多线程的实现(比如 Intel 公司的 River Trail 那一类的项目),这时let表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。

字符串

静态字符串一律使用单引号或者反引号,动态字符串使用双引号

1
2
3
4
5
6
7
8
9
10
// bad
const a = "foobar";
const b = 'foo' + a + 'bar';

// acceptable
const c = `foobar`;

// good
const a = 'foobar';
const b = `foo${a}bar`;

解构赋值

使用数组成员对变量赋值时,优先使用解构赋值。

1
2
3
4
5
6
7
8
const arr = [1, 2, 3, 4];

// bad
const first = arr[0];
const second = arr[1];

// good
const [first, second] = arr;

函数的参数如果是对象的成员,优先使用解构赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
function getFullName(user) {
const firstName = user.firstName;
const lastName = user.lastName;
}

// good
function getFullName(obj) {
const { firstName, lastName } = obj;
}

// best
function getFullName({ firstName, lastName }) {
}

如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。

1
2
3
4
5
6
7
8
9
10
11
// bad
function processInput(input) {
return [left, right, top, bottom];
}

// good
function processInput(input) {
return { left, right, top, bottom };
}

const { left, right } = processInput(input);

对象

单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
const a = { k1: v1, k2: v2, };
const b = {
k1: v1,
k2: v2
};

// good
const a = { k1: v1, k2: v2 };
const b = {
k1: v1,
k2: v2,
};

对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。

1
2
3
4
5
6
7
8
9
10
11
// bad
const a = {};
a.x = 3;

// if reshape unavoidable
const a = {};
Object.assign(a, { x: 3 });

// good
const a = { x: null };
a.x = 3;

对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var ref = 'some value';

// bad
const atom = {
ref: ref,

value: 1,

addValue: function (value) {
return atom.value + value;
},
};

// good
const atom = {
ref,

value: 1,

addValue(value) {
return atom.value + value;
},
};

数组

使用扩展运算符(…)拷贝数组。

1
2
3
4
5
6
7
8
9
10
11
// bad
const len = items.length;
const itemsCopy = [];
let i;

for (i = 0; i < len; i++) {
itemsCopy[i] = items[i];
}

// good
const itemsCopy = [...items];

使用 Array.from 方法,将类似数组的对象转为数组

1
2
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);

函数

立即执行函数可以写成箭头函数的形式。

1
2
3
(() => {
console.log('Welcome to the Internet.');
})();

那些需要使用函数表达式的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。

1
2
3
4
5
6
7
8
9
10
11
12
// bad
[1, 2, 3].map(function (x) {
return x * x;
});

// good
[1, 2, 3].map((x) => {
return x * x;
});

// best
[1, 2, 3].map(x => x * x);

所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。

1
2
3
4
5
6
7
// bad
function divide(a, b, option = false ) {
}

// good
function divide(a, b, { option = false } = {}) {
}

不要在函数体内使用 arguments 变量,使用 rest 运算符(…)代替。因为 rest 运算符显式表明你想要获取参数,而且 arguments 是一个类似数组的对象,而 rest 运算符可以提供一个真正的数组。

1
2
3
4
5
6
7
8
9
10
// bad
function concatenateAll() {
const args = Array.prototype.slice.call(arguments);
return args.join('');
}

// good
function concatenateAll(...args) {
return args.join('');
}

使用默认值语法设置函数参数的默认值

1
2
3
4
5
6
7
8
9
// bad
function handleThings(opts) {
opts = opts || {};
}

// good
function handleThings(opts = {}) {
// ...
}

map结构

注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
let map = new Map(arr);

for (let key of map.keys()) {
console.log(key);
}

for (let value of map.values()) {
console.log(value);
}

for (let item of map.entries()) {
console.log(item[0], item[1]);
}

class

总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// bad
function Queue(contents = []) {
this._queue = [...contents];
}
Queue.prototype.pop = function() {
const value = this._queue[0];
this._queue.splice(0, 1);
return value;
}

// good
class Queue {
constructor(contents = []) {
this._queue = [...contents];
}
pop() {
const value = this._queue[0];
this._queue.splice(0, 1);
return value;
}
}

使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bad
const inherits = require('inherits');
function PeekableQueue(contents) {
Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek = function() {
return this._queue[0];
}

// good
class PeekableQueue extends Queue {
peek() {
return this._queue[0];
}
}

模块

首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import取代require

1
2
3
4
5
6
7
// bad
const moduleA = require('moduleA');
const func1 = moduleA.func1;
const func2 = moduleA.func2;

// good
import { func1, func2 } from 'moduleA';

如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default,export default与普通的export不要同时使用

不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default

1
2
3
4
5
// bad
import * as myObject from './importModule';

// good
import myObject from './importModule';

如果模块默认输出一个函数,函数名的首字母应该小写。

1
2
3
4
function makeStyleGuide() {
}

export default makeStyleGuide;

如果模块默认输出一个对象,对象名的首字母应该大写。

1
2
3
4
5
6
const StyleGuide = {
es6: {
}
};

export default StyleGuide;

ES6的模块和commonJs的不同

  • commonjs模块输出的是一个值的拷贝,es6输出的是值的引用
  • commonjs是运行时加载,es6是编译时输出接口

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');

console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3

上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

1
2
3
4
5
6
7
8
9
10
11
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};

上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。

1
2
3
$ node main.js
3
4

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

1
2
3
4
5
6
7
8
9
10
11
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

上面代码说明,ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化

ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块
由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。

1
2
3
4
5
6
7
8
// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

上面代码中,main.js从lib.js输入变量obj,可以对obj添加属性,但是重新赋值就会报错。因为变量obj指向的地址是只读的,不能重新赋值,这就好比main.js创造了一个名为obj的const变量

最后,export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例

1
2
3
4
5
6
7
8
9
10
11
12
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}

export let c = new C();

上面的脚本mod.js,输出的是一个C的实例。不同的脚本加载这个模块,得到的都是同一个实例