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

4,438 阅读8分钟
原文链接: mp.weixin.qq.com

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.keysObject.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增加了两个实例方法 — padStartpadEnd,这两个方法可以在字符串的首/尾添加其他字符串:

// '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)对象的基础上,也会复制gettersetter方法:

下面的例子用Object.defineProperties拷贝原对象 Car到新对象ElectricCar来展示 Object.assignObject.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被resolvereject后,函数才会恢复执行,

具体看如下代码:

// 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=GreekScript=Greek来搜索所有希腊语字符。

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

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

更多的,Unicode数据库储存了很多种类型的Emoji字符,以Boolean属性EmojiEmoji_ComponentEmoji_PresentationEmoji_ModifierEmoji_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上的实例方法。主要用处是在 resolvereject回调函数执行完后,执行清理任务。

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