1.ECMAScript 2016、2017和2018中所有新特性

作者: xiaozhi 发布时间: 2019-09-05 浏览: 1664 次 编辑

跟踪JavaScript (ECMAScript)中的新内容是很困难的,而且更难找到有用的代码示例。

因此,在本文中将介绍 TC39(最终草案) 在ES2016、ES2017和ES2018中添加的已完成提案中列出的所有18个特性,并给出有用的示例。

1.Array.prototype.includes

include 是数组上的一个简单实例方法,可以轻松查找数组中是否有指定内容(包括 NaN)。

2.求幂操作符

像加法和减法这样的数学运算分别有像 + 和 - 这样运算符。与它们类似,** 运算符通常用于指数运算。在ECMAScript 2016中,引入了 ** 代替 Math.pow。



1.Object.values()

Object.values()是一个类似于Object.keys()的新函数,但返回对象自身属性的所有值,不包括原型链中的任何值。

2.Object.entries()

Object.entries()与Object.keys 类似,但它不是仅返回键,而是以数组方式返回键和值。 这使得在循环中使用对象或将对象转换为映射等操作变得非常简单。

例一:

例二:

3.字符串填充

在String.prototype中添加了两个实例方法:String.prototype.padStart 和 String.prototype.padEnd, 允许在初始字符串的开头或末尾追加/前置空字符串或其他字符串。

'someString'.padStart(numberOfCharcters [,stringForPadding]); 

'5'.padStart(10) // '          5'
'5'.padStart(10, '=*') //'=*=*=*=*=5'
'5'.padEnd(10) // '5         '
'5'.padEnd(10, '=*') //'5=*=*=*=*='

当我们想要在漂亮的打印显示或终端打印进行对齐时,这非常有用。

3.1 padStart 例子:

在下面的例子中,有一个不同长度的数字列表。我们希望在“0”为追加符让所有项长度都为10位,以便显示,我们可以使用padStart(10, '0')轻松实现这一点。

3.2 padEnd 例子:

当我们打印多个不同长度的项目并想要右对齐它们时,padEnd非常有用。

下面的示例是关于padEnd、padStart和 Object.entries 的一个很好的实际示例:

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

####3.3 ⚠️ 注意padStart和padEnd 在Emojis和其他双字节字符上的使用

Emojis和其他双字节字符使用多个unicode字节表示。所以padStart padEnd可能不会像预期的那样工作!⚠️

例如:假设我们要垫达到10个字符的字符串的心❤️emoji。结果如下:

'heart'.padStart(10, "❤️"); // prints.. '❤️❤️❤heart'

这是因为 ❤️ 长2个字节('\ u2764 \ uFE0F')! 单词 heart 是5个字符,所以我们只剩下5个字符来填充。 所以 JS 使用 ('\u2764\uFE0F' ) 填充两颗心并生成 ❤️❤️。 对于最后一个,它只使用 ('\u2764\uFE0F' ) 的第一个字节(\u2764)来生成,所以是 ❤;

4.Object.getOwnPropertyDescriptors

此方法返回给定对象的所有属性的所有属性(包括getter setter set方法),添加这个的主要目的是允许浅 拷贝/克隆到另一个对象中的对象,类似 bject.assign。

Object.assign 浅拷贝除原始对象的 getter 和 setter 方法之外的所有属性。

下面的示例显示了 Object.assign 和 Object.getOwnPropertyDescriptors 以及Object.defineProperties 之间的区别,以将原始对象 Car 复制到新对象 ElectricCar 中。 可以看到使用 Object.getOwnPropertyDescriptors,discount 的 getter 和 setter 函数也被复制到目标对象中。

使用 Object.defineProperties

var Car = {
  name: 'BMW',
  price: 1000000,
  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
 // }
 // 使用 Object.assign 拷贝对象
 const ElectricCar = Object.assign({}, Car);
 //Print details of ElectricCar object's 'discount' property
 console.log(Object.getOwnPropertyDescriptor(ElectricCar, 'discount'));
 // 打印
 // { 
 //   value: undefined,
 //   writable: true,
 //   enumerable: true,
 //   configurable: true 
   
 // }
 // 
 //⚠️请注意,“discount” 属性的 ElectricCar 对象中缺少getter和setter!????????
 
 //Copy Car's properties to ElectricCar2 using Object.defineProperties 
 //and extract Car's properties using Object.getOwnPropertyDescriptors
 const ElectricCar2 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(Car));
 //Print details of ElectricCar2 object's 'discount' property
 console.log(Object.getOwnPropertyDescriptor(ElectricCar2, 'discount'));
 //prints..
 // { get: [Function: get],  ????????????????????????
 //   set: [Function: set],  ????????????????????????
 //   enumerable: true,
 //   configurable: true 
 // }
 // 请注意,在ElectricCar2对象中存在“discount”属性的getter和setter !

5.函数参数的尾逗号

ES2017允许函数的最后一个参数有尾逗号(trailing comma), 此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。这一变化将鼓励开发人员停止丑陋的“行以逗号开头”的习惯。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。

6.Async/Await

到目前为止,个人感受是这是最重要和最有用的功能。 async 函数允许我们不处理回调地狱,并使整个代码看起来很简单。

async 关键字告诉 JavaScript 编译器以不同的方式对待函数。每当编译器到达函数中的 await 关键字时,它就会暂停。它假定 wait 之后的表达式返回一个 promise ,并在进一步移动之前等待该 promise 被 resolved 或 rejected。

在下面的示例中,getAmount 函数调用两个异步函数getUser和getBankBalance。使用 async await更加优雅和简单达到有有序的调用 getUser 与 getBankBalance。

6.1.async 函数默认返回一个 promise

如果您正在等待 async 函数的结果,则需要使用 Promise 的 then 语法来捕获其结果。

在以下示例中,我们希望使用 console.log 来打印结果但是不在 doubleAndAdd 函数里面操作。 因为 async 返回是一个 promise 对象,所以可以在 then 里面执行我们一些打印操作。

6.2 并行调用 async/await

在前面的例子中,我们调用doubleAfterlSec ,但每次我们等待一秒钟(总共2秒)。 相反,我们可以使用 Promise.all 将它并行化为一个并且互不依赖于。

6.3 async/await 函数对错误的处理

在使用async/wait时,有多种方法可以处理错误。

方法一:在函数内使用 try catch

async function doubleAndAdd(a, b) {
  try {
   a = await doubleAfter1Sec(a);
   b = await doubleAfter1Sec(b);
  } catch (e) {
   return NaN; //return something
  }
 return a + b;
 }

 doubleAndAdd('one', 2).then(console.log); // NaN
 doubleAndAdd(1, 2).then(console.log); // 6
 function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
   setTimeout(function() {
    let val = param * 2;
    isNaN(val) ? reject(NaN) : resolve(val);
   }, 1000);
  });
 }

方法二:在 await 后使用 catch 捕获错误

// 方法二:在 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  and logs:  "a" is NaN
 doubleAndAdd(1, 2).then(console.log); // 6
 function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
   setTimeout(function() {
    let val = param * 2;
    isNaN(val) ? reject(NaN) : resolve(val);
   }, 1000);
  });
 }

方法三:在整个的 async-await 函数捕获错误

//方法三:在整个的 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); // ????????????<------- use "catch"
 function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
   setTimeout(function() {
    let val = param * 2;
    isNaN(val) ? reject(NaN) : resolve(val);
   }, 1000);
  });
 }

7.共享内存 和 Atomics

这是一个巨大的、相当高级的特性,是JS引擎的核心增强。

其主要原理是在 JavaScript 中引入某种多线程特性,以便JS开发人员将来可以通过允许自己管理内存而不是让 JS 引擎管理内存来编写高性能的并发程序。

这是通过一种名为 SharedArrayBuffer (即 共享数组缓冲区) 的新类型的全局对象实现的,该对象本质上是将数据存储在共享内存空间中。因此,这些数据可以在主JS线程和web工作线程之间共享。

到目前为止,如果我们想在主 JS 线程和 web 工作者之间共享数据,我们必须复制数据并使用postMessage 将其发送到另一个线程。

你只需使用SharedArrayBuffer,数据就可以立即被主线程和多个web工作线程访问。workers 之间的协调变得更简单和更快(与 postMessage() 相比)。

但是在线程之间共享内存会导致竞争条件。为了帮助避免竞争条件,引入了 “Atomics” 全局对象。 Atomics 提供了各种方法来在线程使用其数据时锁定共享内存。 它还提供了安全地更新该共享内存中的搜索数据的方法。

如果你这对个感兴趣,可以阅读以下文章:

8. Tagged Template literal restriction removed

首先,我们需要知道的什么是 Template literals(“标记的模板文字”),以便更好地理解这个特性。Template literals是一个ES2015特性,它使用反引号包含一个字符串字面量,并且支持嵌入表达式和换行,如:

下面的例子显示,我们的自定义“Tag” 函数 greet 添加了一天中的时间,比如“Good Morning!” “Good afternoon” 等等,取决于一天中的时间字符串的文字和返回自定义字符串。

function greet(hardCodedPartsArray, ...replacementPartsArray) {
  console.log(hardCodedPartsArray); //[ 'Hello ', '!' ]
  console.log(replacementPartsArray); //[ 'Raja' ]
  let str = '';
  hardCodedPartsArray.forEach((string, i) => {
   if (i < replacementPartsArray.length) {
    str += `${string} ${replacementPartsArray[i] || ''}`;
   } else {
    str += `${string} ${timeGreet()}`; //<-- 追加 Good morning/afternoon/evening here
   }
  });
  return str;
 }

 const firstName = 'Raja';
 const greetings = greet`Hello ${firstName}!`; //????????<-- Tagged literal
 console.log(greetings); //'Hello  Raja! Good Morning!' ????
 function timeGreet() {
  const hr = new Date().getHours();
  return hr < 12
   ? 'Good Morning!'
   : hr < 18 ? 'Good Afternoon!' : 'Good Evening!';
 }

现在我们讨论了什么是“标记”函数,许多人希望在不同的领域中使用这个特性,比如在Terminal中用于命令,在组成 uri 的 HTTP 请求中,等等。

** ⚠️ 带标记字符串文字的问题**

问题是ES2015和ES2016规范不允许使用像“\u”(unicode)、“\x”(十六进制)这样的转义字符,除非它们看起来完全像“\ u00A9”或\u{2F804}或\xA9。

因此,如果你有一个内部使用其他域规则(如终端规则)的标记函数,可能需要使用看起来不像\ u0049或\ u {@ F804}的\ ubla123abla,那么你会得到一个语法错误,

function myTagFunc(str) { 
 return { "cooked": "undefined", "raw": str.raw[0] }
} 

var str = myTagFunc `hi \ubla123abla`; //call myTagFunc

str // { cooked: "undefined", raw: "hi \\unicode" }

9.用于正则表达式的“dotall”标志

目前在正则表达式中,虽然点(“.”)应该匹配单个字符,但它不匹配像 \n \r \f 等新行字符。

例如:

//Before
/first.second/.test('first\nsecond'); //false

这种增强使 点 运算符能够匹配任何单个字符。为了确保它不会破坏任何东西,我们需要在创建正则表达式时使用\s标志。

//ECMAScript 2018
/first.second/s.test('first\nsecond'); //true   Notice: /s ????????

更多的方法,请看这里

10.RegExp Named Group Captures

这种增强 RegExp特性借鉴于像Python、Java等其他语言,因此称为“命名组”。这个特性允许编写开发人员以(…)格式为 RegExp 中的组的不同部分提供名称(标识符),使用可以用这个名称轻松地获取他们需要的任何组。

10.1 Named group 的基础用法

在下面的示例中,我们使用 (?) (?) 和 (?) 名称对日期正则表达式的不同部分进行分组。结果对象现在将包含一个groups属性,该属性具有 year、month和 day 的相应值。

let re1 = /(\d{4})-(\d{2})-(\d{2})/;
let result1 = re1.exec('2015-01-02');
console.log(result1);
// [ '2015-01-02', '2015', '01', '02', index: 0, input: '2015-01-02' ]

// ECMAScript 2018
let re2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result2 = re2.exec('2015-01-02');
console.log(result2);

// ["2015-01-02", "2015", "01", "02", index: 0, input: "2015-01-02", 
//    groups: {year: "2015", month: "01", day: "02"}
// ]

console.log(result2.groups.year); // 2015

10.2 在 regex 内使用 Named groups

使用\k<组名>格式来反向引用正则表达式本身中的组,例如:

// 在下面的例子中,我们有一个包合的“水果”组。
// 它既可以配“苹果”,也可以配“橘子”,
// 我们可以使用 “\k<group name>” (\k<fruit>) 来反向引用这个组的结果,
// 所以它可以匹配“=”相同的单词

let sameWords = /(?<fruit>apple|orange)=\k<fruit>/u;

sameWords.test('apple=apple') // true
sameWords.test('orange=orange') // true
sameWords.test('apple=orange') // false

10.3 在 String.prototype.replace 中使用 named groups

在 String.prototype.replace 方法中使用 named groups。所以我们能更快捷的交换词。

例如,把 “firstName, lastName” 改成 “lastName, firstName”。

let re = /(?<firstName>[A-Za-z]+) (?<lastName>[A-Za-z]+$)/u;

'Hello World'.replace(re, `$<lastName>, $<firstName>`) // "World, Hello"

11.对象的 Rest 属性

rest操作 …(三个点)允许挑练我们需要的属性。

11.1 通过 Rest 解构你需要的属性

let { firstName, age, ...remaining } = {
  firstName: '王',
  lastName: '智艺',
  age: 27,
  height: '1.78',
  race: '黄'
}

firstName; // 王
age; // 27
remaining; // { lastName: "智艺", height: "1.78", race: "黄" }

12.对象的扩展属性

扩展 和 解析 的 三个点是一样的,但是不同的是你可以用 扩展 去新建或者组合一个新对象。

扩展 是对齐赋值的右运算符, 而 解构 是左运算符。

const person = { fName: '小明', age: 20 };
const account = { name: '小智', amount: '$1000'};

const personAndAccount = { ...person, ...account };
personAndAccount; // {fName: "小明", age: 20, name: "小智", amount: "$1000"}

13.正则表达式反向(lookbehind)断言

断言(Assertion)是一个对当前匹配位置之前或之后的字符的测试, 它不会实际消耗任何字符,所以断言也被称为“非消耗性匹配”或“非获取匹配”。

正则表达式的断言一共有 4 种形式:

  • (?=pattern) 零宽正向肯定断言(zero-width positive lookahead assertion)

  • (?!pattern) 零宽正向否定断言(zero-width negative lookahead assertion)

  • (?<=pattern) 零宽反向肯定断言(zero-width positive lookbehind assertion)

  • (?<!pattern) 零宽反向否定断言(zero-width negative lookbehind assertion)

你可以使用组(?<=…) 去正向断言,也可以用 (?<!…) 去取反。

正向断言: 我们想确保 # 在 winning 之前。(就是#winning),想正则匹配返回 winning。下面是写法:

反向断言:匹配一个数字,有 € 字符而没有 $ 字符在前面的数字。

更多内容可以参考:S2018 新特征之:正则表达式反向(lookbehind)断言

14. RegExp Unicode Property Escapes

用正则去匹配 Unicode 字符是很不容易的。像 \w , \W , \d 这种只能匹配英文字符和数字。但是其他语言的字符怎么办呢,比如印度语,希腊语?

例如 Unicode 数据库组里把所有的印度语字符,标识为 Script = Devanagari。还有一个属性 Script_Extensions, 值也为 Devanagari。 所以我们可以通过搜索 Script=Devanagari,得到所有的印度语。

Devanagari 可以用于印度的各种语言,如Marathi, Hindi, Sanskrit。

在 ECMAScript 2018 里, 我们可以使用 \p 和 {Script=Devanagari} 匹配那些所有的印度语字符。也就是说 \p{Script=Devanagari} 这样就可以匹配。

//The following matches multiple hindi character
/^\p{Script=Devanagari}+$/u.test('हिन्दी'); //true  
//PS:there are 3 hindi characters h

同理,希腊语的语言是 Script_Extensions 和 Script 的值等于 Greek 。也就是用Script_Extensions=Greek or Script=Greek 这样就可以匹配所有的希腊语,也就是说,我们用 \p{Script=Greek} 匹配所有的希腊语。

进一步说,Unicode 表情库里存了各式各样的布尔值,像 Emoji, Emoji_Component, Emoji_Presentation, Emoji_Modifier, and Emoji_Modifier_Base 的值,都等于 true。所以我们想搜 Emoji 等于 ture,就能搜到所有的表情。

我们用 \p{Emoji} ,\Emoji_Modifier 匹配所有的表情。

参考文献:

  1. ECMAScript 2018 Proposal

  2. https://mathiasbynens.be/notes/es-unicode-property-escapes

15. Promise.prototype.finally()

finally() 是 Promise 新增的一个实例方法。意图是允许在 resolve/reject 之后执行回调。finally 没有返回值,始终会被执行。

让我们看看各种情况。

16.异步迭代(Asynchronous Iteration)

这是一个极其好用的新特性。让我们能够非常容易的创建异步循环代码。

原文: Here are examples of everything new in ECMAScript 2016, 2017, and 2018