
Javascript更新的速度之快难以跟上脚步,配套的教程却少之又少,今天为大家介绍ES2016 ~ ES2018的新增的功能和特性,并配以详细的代码示例。
下面依照JS版本的顺序开始介绍:

1. Array.prototype.includes
includes是数组的实例方法,这个方法的功能很简单:用于判断某一项是否存在数组里,和这个方法功能类似的有 indexOf,两者的区别是indexOf无法判断 NaN,如图:
const arr = [1, 2, 3, 4, NaN];// es5if (arr.indexOf(3) >= 0) { console.log(true)}// es2016if (arr.includes(1)) { console.log(true)}// 注:indexOf不支持检查`NaN`arr.indexOf(NaN) // -1arr.includes(NaN) // true
2. 求幂运算符
求幂运算符: **,用于取代以前的求幂方法 Math.pow,
使用方法如下:
// 之前Math.pow(3, 2) // 9// 现在3**2 // 9

1. Object.values()
Object.values方法和 Object.keys类似,返回类型都是数组,返回的值是对象的值的集合,需要注意一点:两个方法都是返回自身的属性,不包括任何原型链上的属性,如图:
const cars = { BMW: 3, Tesla: 2, Toyota: 1}// es5const vals = Object .keys(cars) .map(key => cars[key]) console.log(vals) // [3, 2, 1]// es2016const 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,遍历:
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:
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 }// es2016const map2 = new Map(Object.entries(cars))console.log(map2) // Map { 'BMW': 3, 'Tesla': 2, 'Toyota': 1 }
3. String padding
String增加了两个实例方法 — padStart和 padEnd,这两个方法可以在字符串的首/尾添加其他字符串:
// 'someStr'.padStart(字符数, [,添加的字符])'hello'.padStart('10', 'a') // 'aaaaahello', 添加了5个字符`a`后一共`10`个字符'hello'.padEnd('10', 'b') // 'hellobbbbb''hello'.padStart('7') // ' hello', 在头部添加两个个空格
3.1 padStart示例
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示例
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的不同。
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后:
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` 创建// 这个函数最后一个参数`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后,函数才会恢复执行,
具体看如下代码:
// 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,000getAmount2('1') // $1,000function 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求值。
具体看如下代码:
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来并行执行这两次调用:
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
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) // NaNdoubleAndAdd(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来处理错误
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 NaNdoubleAndAdd(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函数
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年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以后,允许开发者自定义字符串被嵌入的值。举一个例子,标准的字符串嵌入一个值的方式是:
const userName = '张三'const greetings = `hello ${userName}!`console.log(greetings) // "hello 张三!"
在tagged template literal里,你可以用一个函数通过参数来接收字符串写死的各部分,比如: [‘hello’, ‘!’]和之后被替换为值的变量[‘张三’],最后通过函数返回任何你想要的结果,这个函数被称作Tagged函数,下面 Tagged函数greet来扩展上例中的greetings:
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等换行符。
例如:
// 之前/first.second/.test('first\nsecond'); // false
这个改进使.点操作符匹配任意单个字符。为了保证下面这段代码在任何JS版本都正常工作,我们在结尾加上 /s修饰符
//ECMAScript 2018/first.second/s.test('first\nsecond'); // true 注意: /s 👈🏼
4. 正则表达式捕获Named Group
这个改进带来了在其他语言中比如:Java、Python等已经支持了的有用的正则特性。这个特性允许开发者在正则中为不同的组写格式为(<?name>)的名字标识符,之后可以在匹配的结果里通过名字标识的组来获取对应的值。
4.1 基础示例
// 之前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})/uconst 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>在正则自身来引用之前的组。下面的用例子展示:
// 在这个例子里,我们有一个命名组`fruit`,// 它可以匹配`apple`或者`orange`,我们可以用`\k<group name>`(\k<fruit>)// 来引用之前匹配的这个组// 所以等号两边的值是相等的const sameWords = /(?<fruit>apple|orange)==\k<fruit>/usameWords.test('apple==apple') // truesameWords.test('orange==orange') // truesameWords.test('apple==orange') // false
4.3 在String.prototype.replace里使用命名组
命名组特性已经被添加到replace方法里,所以我们可以轻松地替换字符串了。
例如: 改变”firstName, lastName” 为 “lastName, firstName”:
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来取出你想要使用的属性
let { name, age, ...remaining} = { name: '张三', age: 20, gender: '男', address: 'xxxxx'}name // '张三'age // 20
5.2 你甚至可以移除不想要的属性
// 如果我们想要删除address属性,// 但是我们又不想要遍历对象重新创建新对象// 我们只要简单的解构出这个要移除的属性// 剩下的没有解构的对象// 就是我们想要留下的对象let {address, ...cleanObj} = { name: 'john', address: '北京市海淀区', gender: '男'}cleanObj // {name, gender}
6. 对象的Spread properties
Spread属性看起来和Rest属性很像,也是三个点...操作符,不同的是Spread用于创建新对象。
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:
/(?<=#).*/.test('winning') // false/(?<=#).*/.test('#winning') // true// 之前'#winning'.match(/#.*/)[0] // '#winning'// es2018'#winning'.match(/(?<=#).*/)[0] // 'winning', 没有 #, #只是为了验证
否定断言:比如我们想要取出数字前标志是#,而不是$的数字
'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字符。
// 下面的正在匹配多个北印度语字符// ps: 这里一共有三个北印度语字符/^p{Script=Devanagari}+$/u.test('हिन्दी') // true
相似的,Unicode把所有希腊语字符用属性Script_Extensions (和Script)值为 Greek来分组,所以我们可以用Script_Extensions=Greek或 Script=Greek来搜索所有希腊语字符。
也就是说,我们可以用转义字符\p{Script=Greek}来匹配所有希腊语字符:
/\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字符
/\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回调函数不携带任何参数,不管任何情况下都会被执行。
// Resolve示例let started = truelet 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 = truelet 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的地方是这个循环会等待每个promiseresolve后再去执行下一次循环。
const promises = [ new Promise(resolve => resolve(1)), new Promise(resolve => resolve(2)), new Promise(resolve => resolve(3)),]// 之前// for-of使用正常的同步循环// 不会等待promise resolveasync function test1() { for (const obj of promises) { console.log(obj) // 输出3个promise对象 }}// 之后// for-await-of 使用Async循环// 为每个循环等待promise resolveasync function test2() { for await (const obj of promises) { console.log(obj) // 输出1, 2, 3 }}test1() // promise, promise, promisetest2() // 1, 2, 3