瞌睡龙

技术杂货铺

0%

es2016,2017和2018到底有哪些新功能?

Javascript更新的速度之快难以跟上脚步,配套的教程却少之又少,今天为大家介绍ES2016 ~ ES2018的新增的功能和特性,并配以详细的代码示例。

下面依照JS版本的顺序开始介绍:

ECMAScript 2016

1. Array.prototype.includes

includes是数组的实例方法,这个方法的功能很简单:用于判断某一项是否存在数组里,和这个方法功能类似的有 indexOf,两者的区别是indexOf无法判断 NaN,如图:

1
2
3
4
5
6
7
8
const arr = [1, 2, 3, 4, NaN];
// es5
if (arr.indexOf(3) >= 0) { console.log(true) }
// es2016
if(arr.includes(1)) { console.log(true) }
// 注:indexOf不支持检查`NaN`
arr.indexOf(NaN) // -1
arr.includes(NaN) // true

2. 求幂运算符

求幂运算符: **,用于取代以前的求幂方法 Math.pow,

使用方法如下:

1
2
3
4
// 之前
Math.pow(3, 2) // 9
// 现在
3**2 // 9

ECMAScript 2017

1. Object.values()

Object.values方法和 Object.keys类似,返回类型都是数组,返回的值是对象的值的集合,需要注意一点:两个方法都是返回自身的属性,不包括任何原型链上的属性,如图:

1
2
3
4
5
6
7
const cars = { BMW: 3, Tesla: 2, Toyota: 1 }
// es5
const vals = Object.keys(cars).map(key => cars[key])
console.log(vals) // [3, 2, 1]
// es2016
const values = Object.values(cars)
console.log(values) // [3, 2, 1]

2. Object.entries()

Object.entries()方法有点像 Object.keys和Object.values的结合体,返回类型是数组,同时数组的每一项也是数组 — 包含两项:key和value,这个方法的好处在于你可以通过for of遍历一次取出key/value ;Object.entries()的返回值(object)还可以直接被转为Map:

例1,遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const cars = { BMW: 3, Tesla: 2, Toyota: 1 }
// es5的遍历方式
// 需要把`key`取出来,再遍历
Object.keys(cars).forEach(key => {
console.log(`key: ${key}, value: ${cars[key]}`)
})
// es2017
//
Object.entries(carts);
// [
// ['BMW', 3],
// ['Tesla', 2],
// ['Toyota', 1]
// ]
for (let [key, value] of Object.entries(cars)) {
console.log(`key: ${key}, value: ${cars[key]}`)
}

例2,把object直接转换为Map:

1
2
3
4
5
6
7
8
9
10
const cars = { BMW: 3, Tesla: 2, Toyota: 1 }
// es5const
map1 = new Map()
Object.keys(cars).map(key => {
map1.set(key, cars[key])
})
console.log(map1) // Map { 'BMW': 3, 'Tesla': 2, 'Toyota': 1 }
// es2016
const map2 = new Map(Object.entries(cars))
console.log(map2) // Map { 'BMW': 3, 'Tesla': 2, 'Toyota': 1 }

3. String padding

String增加了两个实例方法 — padStart和 padEnd,这两个方法可以在字符串的首/尾添加其他字符串:

1
2
3
4
// 'someStr'.padStart(字符数, [,添加的字符])
'hello'.padStart('10', 'a') // 'aaaaahello', 添加了5个字符`a`后一共`10`个字符
'hello'.padEnd('10', 'b') // 'hellobbbbb'
'hello'.padStart('7') // ' hello', 在头部添加两个个空格

3.1 padStart示例

1
2
3
4
5
6
7
8
9
10
11
const formatted = [0, 1, 12, 123, 1234, 12345].map(num => num.toString().padStart(10, '0'))
console.log(formatted)
// 输出:
// [
// '0000000000',
// '0000000001',
// '0000000012,'
// '0000000234,'
// '0000001234,'
// '0009012345'
// ]

3.2 padEnd示例

1
2
3
4
5
6
7
8
const cars = { '🚙BMW': '10', '🚘Tesla': '5', '🚖Lamborghini': '0' }
Object.entries(cars).map(([name, count]) => {
console.log(`${name.padEnd(20, ' -')} Count: ${count.padStart(3, '0')}`)
});
//输出:
// 🚙BMW - - - - - - - Count: 010
// 🚘Tesla - - - - - - Count: 005
// 🚖Lamborghini - - - Count: 000

4.Object.getOwnPropertyDescriptors

这个方法的作用是补充Object.assign的功能,在浅拷贝(shallow clone)对象的基础上,也会复制getter和 setter方法:

下面的例子用Object.defineProperties拷贝原对象 Car到新对象ElectricCar来展示 Object.assign和Object.getOwnPropertyDescriptors的不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Car = {
name: 'BMW',
price: 100000,
set discount(x) { this.d = x },
get discount() { return this.d }
};
console.log(Object.getOwnPropertyDescriptor(Car, 'discount'))
// 输出:
// {
// get: [Function: get],
// set: [Function: set],
// enumerable: true,
// configurable: true
// }
const ElectricCar = Object.assign({}, Car)
console.log(Object.getOwnPropertyDescriptor(ElectricCar, 'discount'))
// 输出:
// {
// value: undefined,
// writable: true,
// enumerable: true,
// configurable: true
// }
// 使用`Object.assign`创建`ElectricCar`后,属性`getter`和`setter`丢失了

使用Object.getOwnPropertyDescriptors后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Car = {
name: 'BMW',
price: 100000,
set discount(x) { this.d = x },
get discount() {
return this.d
}
}
const ElectricCar2 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(Car))
// 输出:
// {
// get: [Function: get], <-----👈
// set: [Function: set], <-----👈
// enumerable: true,
// configurable: true
// }

5. 在函数最后一个参数的末尾添加逗号

这是个很小的功能点,在函数形参最后一个参数末尾添加逗号,可以避免git blame提示上一个作者并不存在的改动,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 假设这个函数由 `程序员_1` 创建
// 这个函数最后一个参数`age`后没有逗号
function Person(name, age) { this.name = name; this.age = age }
// 如果 `程序员_2` 这时有了以下修改
function Person(name, age, /* 那么这个`,`逗号也会引起`git blame`认为 `程序员_1` 修改了这一行*/ gender /* 添加了新参数 */) {
// 新添加 this.name = name this.age = age this.gender = gender
// 新添加
}
// es2017对这个混淆的处理办法是:
// 通过 `程序员_1`在`age`末尾添加`,`逗号
// 更新如下:
// 假设这个函数由 `程序员_1` 创建
// 在最后一个参数`age`后添加`,`逗号
function Person(name, age, /* 添加逗号 */) { this.name = name; this.age = age }

6. Async/Await

这个特性是目前为止最重要的一个功能,async函数可以让我们避免频繁调用恶心的 callback,使代码保持干净整洁。

当编译器进入async函数后,遇到 await关键字会暂停执行,可以把await后表达式当作一个 promise,直到promise被resolve或 reject后,函数才会恢复执行,

具体看如下代码:

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
// es5的`Promise`
function getAmount(userId) {
getUser(userId)
.then(getBankBalance)
.then(amount => { console.log(amount) })
}
// es2017的`async`
async function getAmount2(userId) {
var user = await getUser(userId)
var amount = await getBankBalance()
console.log(amount)
}
getAmount('1') // $1,000
getAmount2('1') // $1,000
function getUser(userId) {
return new Promise(resolve => {
setTimeout(() => {
resolve('张三')
}, 1000)
})
}
function getBankBalance() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user === '张三') {
resolve('$1,000')
} else {
resolve('Unknown User')
}
}, 1000)
})
}

6.1 Async函数本身返回一个Promise

因为async函数返回一个promise,所以想要得到async函数的返回值需要对返回的promise进行then求值。

具体看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a)
b = await doubleAfter1Sec(b)
return a + b
}
doubleAndAdd(1, 2).then(console.log) // 5
async function doubleAfter1Sec(param) {
return new Promise(resolve => {
setTimeout(() => {
resolve(param * 2)
}, 1000)
})
}

6.2 并行调用async/await

上一个函数doubleAndAdd里依次调用了两个 async函数,但是每次调用都必须等待1秒,性能很差;因为参数a和参数 b之间并无耦合,所以我们可以使用Promise.all来并行执行这两次调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function doubleAndAdd(a, b) {
// 使用`Promise.all`
// 这个地方使用数组`解构`
// 来得到两次调用的结果
const [a, b] = Promise.all([doubleAfter1Sec(a), doubleAfter1Sec(b)])
return a + b
} doubleAndAdd(1, 2).then(console.log) // 5
async function doubleAfter1Sec(param) {
return new Promise(resolve => {
setTimeout(() => {
resolve(param * 2)
}, 1000)
})
}

6.3 async/await的错误处理

async/await对错误处理有很多方法:

1. 在函数内使用try/catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function doubleAndAdd(a, b) {
try {
a = await doubleAfter1Sec(a)
b = await doubleAfter1Sec(b)
} catch (e) {
return NaN
} return a + b
}
doubleAndAdd('one', 2).then(console.log) // NaN
doubleAndAdd(1, 2).then(console.log) // 5
async function doubleAfter1Sec(param) {
return new Promise(resolve => {
setTimeout(() => {
const val = param * 2
isNaN(val) ? reject(NaN) : resolve(val)
}, 1000)
})
}

2. catch 所有await表达式

因为await表达式返回一个 promise,所以我们可以在await表达式后直接执行 catch来处理错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a).catch(e => console.log(`'a' is NaN`))
b = await doubleAfter1Sec(b).catch(e => console.log(`'b' is NaN`))
if (!a || !b) return NaN
return a + b
}
doubleAndAdd('one', 2).then(console.log) // NaN, "a" is NaN
doubleAndAdd(1, 2).then(console.log) // 5
async function doubleAfter1Sec(param) {
return new Promise(resolve => {
setTimeout(() => {
const val = param * 2
isNaN(val) ? reject(NaN) : resolve(val)
}, 1000)
})
}

3. catch 整个async-await函数

1
2
3
4
5
6
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a)
b = await doubleAfter1Sec(b)
return a + b
}
doubleAndAdd('one', 2).then(console.log).catch(console.log) // 使用catch

ECMAScript 2018

ECMAScript目前在最终稿阶段,将会在2018年6月或7月正式推出。下面介绍的所有特性属于stage-4,即将成为ECMAScript 2018的一部分。

1. 共享内存和原子性

这是JS的一个高级特性,也是JS引擎的核心改进。

共享内存的主要思想是: 把多线程的特性带到JS,为了提高代码的性能和高并发,由之前的JS引擎管理内存变为自己管理内存。

这个特性由一个新的全局对象SharedArrayBuffer来实现,这个对象在一块共享内存区储存数据,JS的主线程和web-worker线程共享这部分数据。

当前,如果我们想要在JS主线程和web-worker线程间共享数据时,必须使用postMessage在不同线程间传递数据,有了 SharedArrayBuffer后,不同的线程可以直接访问这个对象来共享数据。

但是多线程间的共享内存会产生竞态条件,为了避免这种情况,JS引入了原子性的全局对象。这个对象提供了多种方法来保证正在被某个线程访问的内存被锁住,以达到内存安全。

2. Tagged Template literal(带标签的模板字面量?) 限制被移除

首先弄懂一个概念:什么s是Tagged Template literal ?

tagged template literal出现在es2015以后,允许开发者自定义字符串被嵌入的值。举一个例子,标准的字符串嵌入一个值的方式是:

1
const userName = '张三'const greetings = `hello ${userName}!`console.log(greetings) // "hello 张三!"

在tagged template literal里,你可以用一个函数通过参数来接收字符串写死的各部分,比如: [‘hello’, ‘!’]和之后被替换为值的变量[‘张三’],最后通过函数返回任何你想要的结果,这个函数被称作Tagged函数,下面 Tagged函数greet来扩展上例中的greetings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const userName = '张三'
const greetings = `hello ${userName}!`
console.log(greetings)
// "hello 张三!早上好!"
// hardCodedPartsArray: 字符串写死的各部分, [ "hello ", "!" ]
// replacementPartsArray: 字符串里嵌入的变量, [ "张三" ]
function greet(hardCodedPartsArray, ...replacementPartsArray) {
let str = ''
hardCodedPartsArray.forEach((part, i) => {
if (i < replacementPartsArray.length) {
str += `${part}${replacementPartsArray[i] || ''}`
} else {
str += `${part} ${timeGreet()}` // 在结尾添加问候语
}
})
return str
}
function timeGreet() {
const hr = new Date().getHours()
return hr < 12 ? '早上好!' : hr < 18 ? '下午好!' : '晚上好!'
}

3. 正则表达式中的.匹配所有字符

在目前的正则表达式中,虽然.点被认为代表所以字符,实际上它不会匹配像 \n、\r和 \f等换行符。

例如:

1
2
// 之前
/first.second/.test('first\nsecond'); // false

这个改进使.点操作符匹配任意单个字符。为了保证下面这段代码在任何JS版本都正常工作,我们在结尾加上 /s修饰符

1
2
//ECMAScript 2018
/first.second/s.test('first\nsecond'); // true 注意: /s 👈🏼

4. 正则表达式捕获Named Group

这个改进带来了在其他语言中比如:Java、Python等已经支持了的有用的正则特性。这个特性允许开发者在正则中为不同的组写格式为(<?name>)的名字标识符,之后可以在匹配的结果里通过名字标识的组来获取对应的值。

4.1 基础示例

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
// 之前
const re1 = /(\d{4})-(\d{2})-(\d{2})/
const result1 = re1.exec('2015-01-08')
console.log(result1)
// 输出:
// [
// "2015-01-08",
// "2015",
// "01",
// "08",
// index: 0,
// input: "2015-01-08",
// groups: undefined
// ]
// 现在 (es2018)
const re2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
const result2 = re2.exec('2015-01-08')
console.log(result2)
// 输出:
// [
// "2015-01-08",
// "2015",
// "01",
// "08",
// groups: {
// day: "08",
// month: "01",
// year: "2015"
// },
// index: 0,
// input: "2015-01-08",
// ]

4.2 在正则自身使用命名组

我们可以使用格式\k<group name>在正则自身来引用之前的组。下面的用例子展示:

1
2
3
4
5
6
7
8
// 在这个例子里,我们有一个命名组`fruit`,
// 它可以匹配`apple`或者`orange`,我们可以用`\k<group name>`(\k<fruit>)
// 来引用之前匹配的这个组
// 所以等号两边的值是相等的
const sameWords = /(?<fruit>apple|orange)==\k<fruit>/u
sameWords.test('apple==apple') // true
sameWords.test('orange==orange') // true
sameWords.test('apple==orange') // false

4.3 在String.prototype.replace里使用命名组

命名组特性已经被添加到replace方法里,所以我们可以轻松地替换字符串了。

例如: 改变”firstName, lastName” 为 “lastName, firstName”:

1
2
const re = /(?<firstName>[A-Za-z]+) (?<lastName>[A-Za-z]+)/u
'John Lennon'.replace(re, '$<lastName>, $<firstName>') // Lennon John

5. 对象的Rest properties

Rest操作符…(三个点)允许我们取出剩余的对象属性

5.1 使用Rest properties来取出你想要使用的属性

1
2
3
4
5
6
7
8
let { name, age, ...remaining } = {
name: '张三',
age: 20,
gender: '男',
address: 'xxxxx'
}
name // '张三'
age // 20

5.2 你甚至可以移除不想要的属性

1
2
3
4
5
6
7
// 如果我们想要删除address属性,
// 但是我们又不想要遍历对象重新创建新对象
// 我们只要简单的解构出这个要移除的属性
// 剩下的没有解构的对象
// 就是我们想要留下的对象
let {address, ...cleanObj} = { name: 'john', address: '北京市海淀区', gender: '男'}
cleanObj // {name, gender}

6. 对象的Spread properties

Spread属性看起来和Rest属性很像,也是三个点…操作符,不同的是Spread用于创建新对象。

1
2
3
4
const person = {name: 'john', age: 20}
const address = {city: 'Beijing', country: 'china'}
const personWithAddress = { ...person, ...address}
personWithAddress // {name, age, city, country}

7. 正则Lookbehind断言

这个正则的改进允许我们保证在一些字符串之前存在某些字符串。

你可以使用一组(?<=…)(问号,小于等于)寻找后面肯定的断言。

更进一步,你可以使用(?<!…(问号,小于号叹号)寻找后面否定的断言。

肯定断言: 比如我们想要确定在符号#出现在单词 winning前,即#winning,只返回 winning:

1
2
3
4
5
6
/(?<=#).*/.test('winning') // false
/(?<=#).*/.test('#winning') // true
// 之前
'#winning'.match(/#.*/)[0] // '#winning'
// es2018
'#winning'.match(/(?<=#).*/)[0] // 'winning', 没有 #, #只是为了验证

否定断言:比如我们想要取出数字前标志是#,而不是$的数字

1
2
'this is a test signal $1.23'.match(/(?<!\$)\d+\.\d+/) // null
'this is a test signal #2.43'.match(/(?<!\$)\d+\.\d+/)[0] // 2.43

8. 正则Unicode属性转义符

用正则匹配所有的unicode字符很困难。像\w、 \W、\d等只能匹配英文字符和数字,但是出现在其他语言比如希腊语里的数字我们要怎么处理呢?

Unicode属性转义符就是为了解决这个问题。它使Unicode为每个字符添加描述性的metadata。

例如: Unicode数据库把所有北印度语字符归在一个值为Devanagari的属性 Script和另一个值也为的Devanagari的属性 Script_Extensions的组下,所以我们可以通过搜索Script_Extensions来得到所有北印度语字符。

Starting in ECMAScript 2018, we can use \p to escape characters along with {Script=Devanagari} to match all those Indian characters. That is, we can use: \p{Script=Devanagari} in the RegEx to match all Devanagari characters.

从ES2018开始,我们可以使用\p配合{Script=Devanagari}的转义字符来匹配所有北印度语字符,也就是用转义字符 \p{Script=Devanagari}来匹配所有Devanagari字符。

1
2
3
// 下面的正在匹配多个北印度语字符
// ps: 这里一共有三个北印度语字符
/^p{Script=Devanagari}+$/u.test('हिन्दी') // true

相似的,Unicode把所有希腊语字符用属性Script_Extensions (和Script)值为 Greek来分组,所以我们可以用Script_Extensions=Greek或 Script=Greek来搜索所有希腊语字符。

也就是说,我们可以用转义字符\p{Script=Greek}来匹配所有希腊语字符:

1
/\p{Script_Extensions=Greek}/u.test('π') // true

更多的,Unicode数据库储存了很多种类型的Emoji字符,以Boolean属性Emoji、 Emoji_Component、Emoji_Presentation、 Emoji_Modifier和Emoji_Modifier_Base,值为 true来分组,我们可以通过使用Emoji来搜索所有Emoji字符。

也就是通过转义字符\p{Emoji}来匹配各种Emoji字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

/\p{Emoji}/u.test('❤️')
// 下面的例子匹配失败,因为黄色的emoji字符不需要`Emoji_Modifier`
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌️'); //false
// 下面的匹配一个emoji字符,`\p{Emoji}`跟着一个`\p{Emoji_Modifier}`
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌🏽'); //true
// 解释:
// 默认情况下`胜利`的emoji字符是黄色的,
// 如果我们使用棕色、黑色或者其他颜色的变种emoji,
// 它们被当做为原始Emoji字符的变种,使用两个unicode字符来表示,
// 一个代表原始的emoji字符,跟着的一个unicode字符表示颜色
//
// 所以在下面的例子里,即使我们只看到了一个棕色的胜利emoji图标,
// 但是它实际上使用了两个unicode字符,一个是emoji,另一个是棕色。
//
// 在Unicode数据库里,这些颜色有`Emoji_Modifier`属性。
// 所以我们需要使用`\p{Emoji}`和`\p{Emoji_Modifier}`
// 来完整的匹配棕色emoji
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌🏽'); //true

Lastly, we can use capital “P”(\P ) escape character instead of small p (\p ), to negate the matches.

最后,我们可以使用大写P(\P)转义字符来匹配和小写\p匹配内容相反的内容。

8. Promise.prototype.finally()

finally()是新加到Promise上的实例方法。主要用处是在 resolve或reject回调函数执行完后,执行清理任务。

finally回调函数不携带任何参数,不管任何情况下都会被执行。

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
49
50
51
// Resolve示例
let started = true
let myPromise = new Promise((resolve, reject) => {
resolve('all good')
}).then(val => {
console.log(val) // 'all good'
}).catch(e => {
console.log(e) // 跳过
}).finally(() => {
console.log('这个函数总是会被执行')
started = false // 清理
})
// Reject示例
let started = true
let myPromise = new Promise((resolve, reject) => {
reject('reject apple')
}).then(val => {
console.log(val) // 'reject apple'
}).catch(e => {
console.log(e) // 跳过
}).finally(() => {
console.log('这个函数总是会被执行')
started = false // 清理
})
// 错误case 1
// 从Promise抛出错误
let started = truelet
myPromise = new Promise((resolve, reject) => {
throw new Error('error')
}).then(val => {
console.log(val) // 跳过
}).catch(e => {
console.log(e) // 因为有error,所以catch被调用
}).finally(() => {
console.log('这个函数总是会被执行')
started = false // 清理
})
// 错误case 2
// 从`catch` case 抛出错误
let started = truelet
myPromise = new Promise((resolve, reject) => {
throw new Error('something happened')
}).then(val => {
console.log(val) // 跳过
}).catch(e => {
throw new Error('throw another error')
}).finally(() => {
console.log('这个函数总是会被执行')
started = false // 清理
// 注意,从*catch*里抛出的错误需要在其他地方处理
})

9. 异步循环

这是一个特别有用的特性。根本上讲,它允许我们轻易地在异步函数里创建循环。

这个特性添加了一个新的for-await-of循环,允许我们在一个循环里调用返回promise(或者是每一项为promise的数组)的异步函数。

最cool的地方是这个循环会等待每个 promise resolve 后再去执行下一次循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const promises = [
new Promise(resolve => resolve(1)),
new Promise(resolve => resolve(2)),
new Promise(resolve => resolve(3)),
]
// 之前
// for-of使用正常的同步循环
// 不会等待promise resolve
async function test1() {
for (const obj of promises) {
console.log(obj) // 输出3个promise对象
}
}
// 之后
// for-await-of 使用Async循环
// 为每个循环等待promise resolve
async function test2() {
for await (const obj of promises) {
console.log(obj) // 输出1, 2, 3
}
}
test1() // promise, promise, promise
test2() // 1, 2, 3

欢迎关注我的其它发布渠道