JS 开发者必须知道的十个 ES6 新特性
这篇文章会给你简单介绍一下ES6。如果你还不知道什么是ES6的话,它是JavaScript一个新的实现,如果你是一个忙碌的JavaScript开发者(但谁不是呢),那么继续读下去吧,看看当今最热门的语言——JavaScript的新一代实现中,最棒的十个特性。
这是为忙碌的开发者准备的ES6中最棒的十个特性(无特定顺序):
- 默认参数
- 模版表达式
- 多行字符串
- 拆包表达式
- 改进的对象表达式
- 箭头函数
=&>
- Promise
- 块级作用域的
let
和const
- 类
- 模块化
注意:这个列表十分主观并且带有偏见,其他未上榜的特性并不是因为没有作用,我这么做只是单纯的希望将这份列表中的项目保持在十个。
首先,一个简单的JavaScript时间线,不了解历史的人也无法创造历史。
- 1995年:JavaScript以LiveScript之名诞生
- 1997年:ECMAScript标准确立
- 1999年:ES3发布,IE5非常生气
- 2000年-2005年:
XMLHttpRequest
,熟知为AJAX
,在如Outlook Web Access(2002)、Oddpost(2002)、Gmail(2004)、Google Maps(2005)中得到了广泛的应用 - 2009年:ES5发布(这是我们目前用的最多的版本),带来了
forEach
/Object.keys
/Object.create
(特地为Douglas Crockford所造,JSON标准创建者) ,还有JSON标准。
历史课上完了,我们回来讲编程。
1. ES6中的默认参数
还记得我们以前要这样子来定义默认参数:
1
2
3
4
5
6
|
var link = function (height, color, url) {
var height = height || 50
var color = color || 'red'
var url = url || 'http://azat.co'
...
}
|
这样做一直都没什么问题,直到参数的值为0
,因为0
在JavaScript中算是false
值,它会直接变成后面硬编码的值而不是0
本身。当然了,谁要用0
来传值啊(讽刺脸)?所以我们也忽略了这个瑕疵,沿用了这个逻辑,否则的话只能…..没有否则!在ES6中,我们可以把这些默认值直接放在函数签名中。
1
2
3
|
var link = function(height = 50, color = 'red', url = 'http://azat.co') {
...
}
|
对了,这个语法和Ruby很像!
2. ES6中的模版表达式
模版表达式在其他语言中一般是为了在模版字符串中输出变量,所以在ES5中,我们非得把字符串破开变成这样:
1
2
|
var name = 'Your name is ' + first + ' ' + last + '.'
var url = 'http://localhost:3000/api/messages/' + id
|
幸运的是在ES6中我们有了新语法,在反引号包裹的字符串中,使用${NAME}语法来表示模板字符:
1
2
|
var name = `Your name is ${first} ${last}`
var url = `http://localhost:3000/api/messages/${id}`
|
3. ES6中的多行字符串
另一个好吃的语法糖就是多行字符串,以前我们的实现是像这样的:
1
2
3
4
5
6
7
8
|
var roadPoem = 'Then took the other, as just as fair,nt'
+ 'And having perhaps the better claimnt'
+ 'Because it was grassy and wanted wear,nt'
+ 'Though as for that the passing therent'
+ 'Had worn them really about the same,nt'
var fourAgreements = 'You have the right to be you.n
You can only be you when you do your best.'
|
但是在ES6中,只要充分利用反引号。
1
2
3
4
5
6
7
8
|
var roadPoem = `Then took the other, as just as fair,
And having perhaps the better claim
Because it was grassy and wanted wear,
Though as for that the passing there
Had worn them really about the same,`
var fourAgreements = `You have the right to be you.
You can only be you when you do your best.`
|
4. ES6中的拆包表达式
拆包可能是一个比较难理解的概念,因为这里面真的是有魔法发生。假如说你有一个简单的赋值表达式,把对象中的house
的mouse
赋值为house
和mouse
的变量。
1
2
3
|
var data = $('body').data(), // 假设data中有mouse和house的值
house = data.house,
mouse = data.mouse
|
另一个拆包的实例(Node.js):
1
2
3
4
5
|
var jsonMiddleware = require('body-parser').json
var body = req.body, // body中有用户名和密码值
username = body.username,
password = body.password
|
但是在ES6中我们可以用以下语句替换:
1
2
3
4
5
|
var { house, mouse} = $('body').data() // 我们会拿到house和mouse的值的
var {jsonMiddleware} = require('body-parser')
var {username, password} = req.body
|
甚至在数组中也能用,简直疯狂!
1
2
|
var [col1, col2] = $('.column'),
[line1, line2, line3, , line5] = file.split('n')
|
习惯拆包语法可能需要一些时间,但是这绝对是糖衣炮弹。
5. ES6中改进的对象表达式
你能用对象表达式所做的是超乎想象的!类定义的方法从ES5中一个美化版的JSON,进化到ES6中更像类的构造。
这是一个ES5中典型的对象表达式,定义了一些方法和属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var serviceBase = {port: 3000, url: 'azat.co'},
getAccounts = function(){return [1,2,3]}
var accountServiceES5 = {
port: serviceBase.port,
url: serviceBase.url,
getAccounts: getAccounts,
toString: function() {
return JSON.stringify(this.valueOf())
},
getUrl: function() {return 'http://' + this.url + ':' + this.port},
valueOf_1_2_3: getAccounts()
}
|
如果你想做的好看一点,我们可以用Object.create
方法来让 serviceBase
成为 accountServiceES5
的 prototype
从而实现继承。
1
2
3
4
5
6
7
8
9
|
var accountServiceES5ObjectCreate = Object.create(serviceBase)
var accountServiceES5ObjectCreate = {
getAccounts: getAccounts,
toString: function() {
return JSON.stringify(this.valueOf())
},
getUrl: function() {return 'http://' + this.url + ':' + this.port},
valueOf_1_2_3: getAccounts()
}
|
我知道 accountServiceES5ObjectCreate
和 accountServiceES5
是不完全相同的。因为一个对象 accountServiceES5
会有如下所示的 __proto__
属性:
但对于这个示例,我们就把这两者考虑为相同的。所以在ES6的对象表达式中,我们把getAccounts: getAccounts
简化为getAccounts,
,并且我们还可以用__proto__
直接设置prototype
,这样听起来合理的多。(不过并不是用proto
)
1
2
3
4
5
|
var serviceBase = {port: 3000, url: 'azat.co'},
getAccounts = function(){return [1,2,3]}
var accountService = {
__proto__: serviceBase,
getAccounts,
|
还有,我们可以调用 super
和动态索引(valueOf_1_2_3
)
1
2
3
4
5
6
7
8
|
// 续上段代码
toString() {
return JSON.stringify((super.valueOf()))
},
getUrl() {return 'http://' + this.url + ':' + this.port},
[ 'valueOf_' + getAccounts().join('_') ]: getAccounts()
};
console.log(accountService)
|
这是对老旧的对象表达式一个很大的改进!
6. ES6中的箭头函数
这或许是我最想要的一个特性,我爱 CoffeeScript 就是因为他胖胖的箭头(=&>
相对于-&>
),现在ES6中也有了。这些箭头最神奇的地方在于他会让你写正确的代码。比如,this
在上下文和函数中的值应当是相同的,它不会变化,通常变化的原因都是因为你创建了闭包。
使用箭头函数可以让我们不再用that = this
或者self = this
或者_this = this
或者.bind(this)
这样的代码,比如,这些代码在ES5中就特别丑。
1
2
3
4
|
var _this = this
$('.btn').click(function(event){
_this.sendData()
})
|
这是在ES6中去掉_this = this
之后:
1
2
3
|
$('.btn').click((event) =>{
this.sendData()
})
|
可惜的是,ES6委员会觉得再加上瘦箭头(-&>
)的话就对我们太好了,所以他们留下了一个老旧的function
。(瘦箭头在CoffeeScript中的作用就像ES5/6中一样)
1
2
3
4
5
6
7
8
9
10
|
var logUpperCase = function() {
var _this = this
this.string = this.string.toUpperCase()
return function () {
return console.log(_this.string)
}
}
logUpperCase.call({ string: 'es6 rocks' })()
|
在ES6中我们无需_this
1
2
3
4
5
6
|
var logUpperCase = function() {
this.string = this.string.toUpperCase()
return () => console.log(this.string)
}
logUpperCase.call({ string: 'es6 rocks' })()
|
注意,在ES6中你可以合理的把箭头函数和旧式 function
函数混用。当箭头函数所在语句只有一行时,它就会变成一个表达式,它会直接返回这个语句的值。但是如果你有多行语句,你就要明确的使用return
。
这是ES5中利用messages
数组创建一个数组的代码:
1
2
3
4
|
var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9']
var messages = ids.map(function (value) {
return 'ID is ' + value // 显式返回
});
|
在ES6中会变成这样:
1
2
|
var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9']
var messages = ids.map(value => `ID is ${value}`) // 隐式返回
|
注意到我用了字符串模版吗,又一个从CoffeeScript中来的功能,我爱它们!
在只有一个参数的函数签名中,括号是可有可无的,但是如果多于一个参数时就要加上。
1
2
|
var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9']
var messages = ids.map((value, index, list) => `ID of ${index} element is ${value} `) // 隐式返回
|
7. ES6中的Promise
Promise是一个有争议的话题。现在有很多Promise实现,语法也大致相同,比如q
/ bluebird
/ deferred.js
/ vow
/ avow
/ jquery deferred
等等。其他人说我们并不需要Promise,异步,回调和generator
之类的就很好。庆幸的是,现在在ES6中终于有一个标准的Promise实现。
我们来看一个相当微不足道的延迟异步执行,用setTimeout
实现
1
2
3
|
setTimeout(function(){
console.log('Yay!')
}, 1000)
|
我们可以用ES6中的Promise重写:
1
2
3
4
5
|
var wait1000 = new Promise(function(resolve, reject) {
setTimeout(resolve, 1000)
}).then(function() {
console.log('Yay!')
})
|
或者用ES6的箭头函数:
1
2
3
4
5
|
var wait1000 = new Promise((resolve, reject)=> {
setTimeout(resolve, 1000)
}).then(()=> {
console.log('Yay!')
})
|
到现在为止,我们只是单纯增加了代码的行数,还明显没有带来任何好处,你说的对。但是如果我们有更多复杂的逻辑内嵌在setTimeout()
中的回调时好处就来了:
1
2
3
4
5
6
|
setTimeout(function(){
console.log('Yay!')
setTimeout(function(){
console.log('Wheeyee!')
}, 1000)
}, 1000)
|
可以用ES6中的Promise重写:
1
2
3
4
5
6
7
8
9
10
|
var wait1000 = ()=> new Promise((resolve, reject)=> {setTimeout(resolve, 1000)})
wait1000()
.then(function() {
console.log('Yay!')
return wait1000()
})
.then(function() {
console.log('Wheeyee!')
});
|
还是无法相信Promise比普通回调要好?我也不信。我想一旦知道了回调这个方法它就会在你脑中萦绕,额外的复杂的Promise也没有必要存在了。
不论怎么说,ES6中的Promise是为会欣赏的人准备的,Promise有一个不错的失败-捕捉
回调机制,看看这篇文章吧,里面有更多关于Promise的信息。ES6 Promise介绍
8. 块级作用域的let
和const
你可能早就听过对ES6中的let
那些奇怪的传说,我记得我第一次到伦敦时为那些TO LET牌子感到非常困惑。但是ES6中的let
和出租无关,这不算是语法糖,它很复杂。let
是一个更新的var
,可以让你把变量作用域限制在当前块里。我们用{}
来定义块,但是在ES5中这些花括号起不到任何作用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
function calculateTotalAmount (vip) {
var amount = 0
if (vip) {
var amount = 1
}
{ // 让块来的更疯狂
var amount = 100
{
var amount = 1000
}
}
return amount
}
console.log(calculateTotalAmount(true))
|
运行结果将会是1000
。天啊!这是多大的一个Bug。在ES6中,我们用let
来限制变量作用域为函数内。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
function calculateTotalAmount (vip) {
var amount = 0 // 或许应该用let, 但你可以混用
if (vip) {
let amount = 1 // 第一个数量为 0
}
{ // 更多的块
let amount = 100 // 第一个数量为 0
{
let amount = 1000 // 第一个数量为 0
}
}
return amount
}
console.log(calculateTotalAmount(true))
|
运行结果是0
,因为在if
块中也有let
。如果什么都没有的话(amount=1
),那么结果将会是1
。
说到const
,事情就简单多了。他仅仅产生是一个不可变的变量,并且他的作用域也像let
一样只有块级。为了演示,这里有定义了一堆常量,并且由于作用域的原因,这些定义都是有效的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
function calculateTotalAmount (vip) {
const amount = 0
if (vip) {
const amount = 1
}
{ // 更多的块
const amount = 100
{
const amount = 1000
}
}
return amount
}
console.log(calculateTotalAmount(true))
|
依我愚见,let
和const
让这门语言变得更加复杂,没有这些的时候我们只有一条路可以走,但是现在可以要考虑更多的情景。;-(
9. ES6中的类
如果你喜欢面向对象编程,那么你会特别喜欢这个特性。他让你编写和继承类时就跟在Facebook上发一个评论这么简单。
在ES5中,因为没有class
关键字(但它是毫无作用的保留字),类的创建和使用是让人十分痛苦的事情。更惨的是,很多伪类的实现像pseude-classical, classical, functional让人越来越摸不着头脑,为JavaScript的信仰战争火上浇油。
我不会给你展示在ES5中怎么去编写一个类(是啦是啦从对象可以衍生出来其他的类和对象),因为有太多方法去完成。我们直接看ES6的示例,告诉你ES6的类会用prototype
来实现而不是function
。现在有一个baseModel
类,其中我们可以定义构造函数和getName()
方法。
1
2
3
4
5
6
7
8
9
10
11
12
|
class baseModel {
constructor(options = {}, data = []) { // class constructor
this.name = 'Base'
this.url = 'http://azat.co/api'
this.data = data
this.options = options
}
getName() { // class method
console.log(`Class name: ${this.name}`)
}
}
|
注意到我给options
和data
用了默认参数,而且方法名再也不用加上function
或者:
了。还有一个很大的区别,你不能像构造函数里面一样向this.Name
指派值。怎么说呢,和函数
有相同缩进的代码里,你不能向name
赋值。如果有这个需要的话,在构造函数里面完成。
使用NAME extends PARENT_NAME
语法,AccountModel
从baseModel
继承而来。
1
2
|
class AccountModel extends baseModel {
constructor(options, data) {
|
调用父类构造函数时,只需带上参数轻松的调用super()
方法。
1
2
3
4
|
super({private: true}, ['32113123123', '524214691']) //call the parent method with super
this.name = 'Account Model'
this.url +='/accounts/'
}
|
想要高级一点的话,你可以像这样弄一个getter
方法,这样accountsData
就会变成一个属性。
1
2
3
4
5
|
get accountsData() { // 返回计算后的数据
// ... make XHR
return this.data
}
}
|
现在你要怎么用这个魔咒,很简单,就跟让三岁小孩相信圣诞老人存在一样。
1
2
3
|
let accounts = new AccountModel(5)
accounts.getName()
console.log('Data is %s', accounts.accountsData)
|
如果好奇输出结果的话:
1
2
|
Class name: Account Model
Data is 32113123123,524214691
|
10. ES6中的模块化
你可能知道,ES6之前JavaScript并没有对模块化有过原生的支持,人们想出来AMD
,RequireJS
,CommenJS
等等,现在终于有import
和export
运算符来实现了。
ES5中你会用script
标签和IIFE(立即执行函数)
,或者是其他的像AMD
之类的库,但是ES6中你可以用export
来暴露你的类。我是喜欢Node.js的人,所以我用和Node.js语法一样的CommonJS
,然后用Browserfy来浏览器化。现在我们有一个port
变量和getAccounts
方法,在ES5中:
1
2
3
4
5
6
|
module.exports = {
port: 3000,
getAccounts: function() {
...
}
}
|
在ES5的main.js
中,用require('模块')
来导入:
1
2
|
var service = require('module.js')
console.log(service.port) // 3000
|
但是在ES6中,我们用export
和import
。比如这是ES6中的module.js
文件:
1
2
3
4
|
export var port = 3000
export function getAccounts(url) {
...
}
|
在需要引入的main.js
文件中,可以用import {名称} from '模块'
语法:
1
2
|
import {port, getAccounts} from 'module'
console.log(port) // 3000
|
或者就直接在main.js
中引入所有的变量:
1
2
|
import * as service from 'module'
console.log(service.port) // 3000
|
个人来说,我觉得这样的模块化有些搞不懂。确实,这样会更传神一些 。但是Node.js中的模块不会马上就改过来,浏览器和服务器的代码最好是用同样的标准,所以目前我还是会坚持CommonJS/Node.js
的方式。
目前来说浏览器对ES6的支持还遥遥无期(本文写作时),所以你需要一些像jspm这样的工具来用ES6的模块。
想要了解更多ES6中的模块化和例子的话,来看这篇文章,不管怎么说,写现代化的JavaScript吧!
怎么样可以在今天就用上ES6(Babel)
ES6标准已经敲定,但还未被所有浏览器支持(Firefox的ES6功能一览),如果想马上就用上ES6,需要一个像Babel这样的编译器。你可以把他当独立工具用,也可以将他集成到构建系统里,Babel对Gulp
,Grunt
和Webpack
都有对应的插件。
安装Gulp插件示例:
1
|
$ npm install --save-dev gulp-babel
|
在gulpfile.js
中,定义这么一个任务,将src
目录下的app.js
文件编译到build
目录下:
1
2
3
4
5
6
7
8
|
var gulp = require('gulp'),
babel = require('gulp-babel')
gulp.task('build', function () {
return gulp.src('src/app.js')
.pipe(babel())
.pipe(gulp.dest('build'))
})
|
Node.js和ES6
对于Node.js,你可以用构建工具或者直接用独立模块babel-core
:
1
|
$ npm install --save-dev babel-core
|
然后在Node.js中调用这个函数:
1
|
require('babel-core').transform(es5Code, options)
|
ES6的一些总结
ES6中还有很多你可能都用不上(至少现在用不上)的可圈可点的特性,以下无特定顺序:
Math
/Number
/String
/Array
/Object
中新的方法- 二进制和八进制数据类型
- 自动展开多余参数
For of
循环(又见面了CoffeeScript)Symbols
- 尾部调用优化
generator
- 更新的数据结构(如
Map
和Set
)