ECMAScript 2016, 2017, 和2018中新增功能的示例

以下是 ECMAScript 2016, 2017, 和 2018 中新增功能的示例

原文链接

要跟上 JavaScript(ECMAScript) 更新的脚步很难,更难的是找到有用的代码示例。

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

这篇文章很长,不过应该很容易读,可以当成一篇“Netflix binge reading”。我保证你读完后将对这些新特性有很多的了解。

好,我们一个一个来看看。

ECMAScript 2016

  • 1. Array.prototype.includes

includes 是 Array 的一个实例方法,用来轻松查找某个项是否在 Array 中(包括 NaN,这点 indexOf 做不到)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const arr = [ 1, 2, 3, 4, NaN];

// es2016不使用
if(arr.indexOf(3) >= 0){
console.log(true);
}

// 使用
if(arr.includes(3)){
console.log(true);
}

//ps: 注意 indexOf 是不支持查找NaN的
arr.includes(NaN) // true
arr.indexOf(NaN) // -1 (indexOf 不支持 NaN)

冷知识:JavaScript 规范制定者本想给它取名叫 contains,但这显然已被 Mootools 使用,因此他们使用了 includes

  • 2. 幂运算符

数学运算如加法和减法分别有 +- 等运算符。与他们类似,** 运算符通常用于幂运算。在 ECMAScript 2016 中引入了 **,取代 Math.pow

1
2
3
4
5
// 不使用
Math.pow(7,2) //49

// 使用
7**2 //49

ECMAScript 2017

  • 1. Object.values()

Object.value() 是一个新的函数,它与 Object.keys() 类似,但返回的事 Object 所有属性的值(不包括来自原型链的属性)。

1
2
3
4
5
6
7
8
9
10
11
12
const cars = { 'BMW': 3, 'Tesla': 2, 'Toyota': 1 };

// ES2015
// ES2017 不使用
const vals = Object.keys(cars).map(key => cars[key]);
console.log(vals); //[3, 2, 1]


// ES2017 and 未来
// 使用
const values = Object.values(cars);
console.log(values); // [3, 2, 1]
  • 2. Object.entries()

Object.entries()Object.keys 相关,但不是仅返回 keys,而是以数组方式返回 keys 和 values。这让你可以很容易地遍历对象,或者把对象转换为 Maps。

例 1:

1
2
3
4
5
6
7
8
9
10
11
12
const cars = { 'BMW': 3, 'Tesla': 2, 'Toyota': 1 };
// ES5.1
// 而不是提取键然后再循环
Object.keys(cars).forEarch(function(key) {
console.log('key: ' + key + 'value: ' + cars[key]);
})

// ES2017 (ES8)
// 使用 Object.entries
for(let [key value] of Object.entries(cars)) {
console.log(`key: ${key} value: ${value}`);
}

例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const cars = { 'BMW': 3, 'Tesla': 2, 'Toyota': 1 };

// ES2015
// 不使用
// 获取对象键,然后添加每个项目以在循环中映射

const maps = new Map();
Object.keys(cars).forEarch(key => {
map1.set(key, cars[key]);
})

console.log(map1) // Map { 'BMW' => 3, 'Tesla' => 2, 'Toyota' => 1}

// ES2017 and 以后
// 使用
const map = new Map(Object.entries(cars));

console.log(map) //Map { 'BMW' => 3, 'Tesla' => 2, 'Toyota' => 1}
  • 3. 字符串填充

String 增加了两个实例方法 – String.prototype.padStartString.prototype.padEnd,用于在原字符串的开头或结尾添加一段字符串或空字符串。

1
2
3
4
5
'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') 轻松实现这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ES2017
// 如果你有一个不同长度的项目列表,并希望格式化它们的显示目的,你可以使用padStart

const formatted = [0, 1, 12, 123, 1234, 12345].map(num =>
num.toString().padStart(10, '0') // 添加 0 直到长度为 10
);

console.log(formatted);

// 打印
// [
// '0000000000',
// '0000000001',
// '0000000012',
// '0000000123',
// '0000001234',
// '0000012345',
// ]
  • 3.2 padEnd 例子:

我们打印多个不同长度的项目并想要右对齐它们时,padEnd 真的很方便。

下面的例子很好地展示了如何运用 padEndpadStartObject.entries 产生漂亮的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const cars = {
'🚙BMW': '10',
'🚘Tesla': '5',
'🚖Lamborghini': '0'
}
Object.entries(cars).map(([name, count]) => {
//padEnd appends ' -' until the name becomes 20 characters
//padStart prepends '0' until the count becomes 3 characters.
console.log(`${name.padEnd(20, ' -')} Count: ${count.padStart(3, '0')}`)
});
// 打印结果..
// 🚙BMW - - - - - - - Count: 010
// 🚘Tesla - - - - - - Count: 005
// 🚖Lamborghini - - - Count: 000
  • 3.3 ⚠️ Emoji 和其他双字节字符上的 padStart 和 padEnd

Emoji 和其他双字节字符使用多个 unicode 字节表示。所以 padStartpadEnd 可能无法按预期工作!⚠️

例如:假设我们正在尝试用 ❤ ️表情符号填充字符串 heart 以达到10个字符。结果如下所示:

1
2
3
// 注意,不是5颗心,只有2颗心和1颗看起来很奇怪的心!

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

这是因为 ❤ ️长 2 个字符('\u2764\uFE0F')!单词 heart 本身是 5 个字符,我们要填充剩下的 5 个字符。所以 JS 先填充了两个 '\u2764\uFE0F' (也就是 ❤️❤️️) ️,最后剩下一个字节填充了 \ u2764,而它对应的是小心形 ❤。

所以结果是: ❤️❤️❤heart

PS:您可以使用此链接 查看 unicode 字符转换

  • 4. Object.getOwnPropertyDescriptors

此方法返回给定对象的所有属性的所有详细信息(包括 get set 方法)。主要目的是为了在浅拷贝/克隆一个对象到另一个对象中时,能把 getter 和 setter 也复制过去,因为 Object.assign 无法做到这一点。

Object.assign 浅克隆除原始源对象的 getter 和 setter 函数以外的所有详细信息。

以下示例展示了分别使用 Object.assignObject.getOwnPropertyDescriptors(配合 Object.defineProperties) 以将原始对象 Car 复制到新对象 ElectricCar 时的区别。你会发现使用 Object.getOwnPropertyDescriptors 会将 discount 的 getter 和 setter 也被复制到目标对象中。

之前…
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
// 之前...
var Car = {
name: 'BMW',
price: 1000000,
set discount(x) {
this.d = x;
},
get discount() {
return this.d;
},
};

// 打印Car对象的“discount”属性的详细信息
console.log(Object.getOwnPropertyDescriptor(Car, 'discount'));
// 打印结果..
// {
// get: [Function: get],
// set: [Function: set],
// enumerable: true,
// configurable: true
// }

// 使用Object.assign将Car的属性复制到ElectricCar
const ElectricCar = Object.assign({}, Car);

//打印ElectricCar对象的“discount”属性的详细信息
console.log(Object.getOwnPropertyDescriptor(ElectricCar, 'discount'));

// 打印
// {
// value: undefined,
// writable: true,
// enumerable: true,
// configurable: true
// }

// ⚠️ 请注意ElectricCar对象中的“discount”属性中缺少getter和setter!👎👎
以后…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 以后...
var Car = {
name: 'BMW',
price: 1000000,
set discount(x) {
this.d = x;
},
get discount() {
return this.d;
},
};
// 使用Object.defineProperties将Car的属性复制到ElectricCar2
// 并使用Object.getOwnPropertyDescriptors提取Car的属性

const ElectricCar2 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(Car));
// 打印ElectricCar2对象“discount”属性的详细信息
console.log(Object.getOwnPropertyDescriptor(ElectricCar2, 'discount'));
// prints..
// { get: [Function: get], 👈🏼👈🏼👈🏼
// set: [Function: set], 👈🏼👈🏼👈🏼
// enumerable: true,
// configurable: true
// }
// 请注意,getter和setter存在于ElectricCar2对象中,用于'discount'属性!
  • 5. 在函数参数中添加尾后逗号(trailing commas)

这是一个小的更新,它允许我们在最后一个函数参数后面加上尾随逗号。有什么用呢? 当你使用 git blame 时,这能确保只有新增的参数被显示出来。

以下示例展示了问题和解决方案:

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
// 问题

// #1 开发者创建
function Person(
name,
age
) {
this.name = name;
this.age = age;
}

// #2 开发者添加address
function Person(
name,
age, // 添加尾随逗号 <--------因为逗号,开发人员会被git等工具所警告
address //添加新的参数
) {
this.name = name;
this.age = age;
this.address = address; //添加的这行
}

// ES2017 解决
// 允许#1开发者添加尾随逗号
// #1 创建如下
function Person(
name,
ange, //<------- 由于尾随逗号,#2开发者不需要改变这行
) {
this.name = name;
this.age = age;
}

注意:您也可以在调用函数时使用尾后逗号!

  • 6. Async/Await

这个特性,在我看来是迄今为止最重要也是最有用的特性。异步函数把我们从回调地狱中解放出来,并使整个代码看起来很简洁。

async 关键字告诉 JavaScript 编译器以不同的方式处理函数,编译器在该函数内到达 await 关键字时暂停。它假设 await 之后的表达式返回一个 Promise,并等到 Promise 被 resolve 或 reject 后才继续前进。

在下面的例子中,getAmount 函数调用两个异步函数 getUsergetBankBalance。我们可以用 Promise 做到这一点,但使用 async await 更优雅和简洁。

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
// 不使用
// ES2015 Promise

function getAmount(userId) {
getUser(userId)
.then(getBankBalance)
.then(amount => {
console.log(amount);
});
}

// 使用...
// ES2017

async function getAmount2(userId) {
var user = await getUser(userId);
var amount = await getBankBalance(user);
console.log(amount);
}

getAmount('1'); //$1,000

getAmount2('1'); //$1,000

function getUser(userId) {
return new Promise(resolve => {
setTimeout(() => {
resolve('john')
}, 1000)
});
}

function getBankBalance(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if(user == 'john') {
resolve('$1,000');
} else {
reject(unknown user);
}
}, 1000)
});
}
  • 6.1 异步函数本身返回一个 Promise

如果您要等待异步函数的结果,则需要使用 Promise 的 then 语法来捕获它。

在下面的例子中,我们想使用 console.log 在 doubleAndAdd 的外部打印出它的结果。所以我们使用 then 来等待并获取它的结果传递给 console.log

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

// 异步函数本身返回一个Promise
async function doubleAndAdd(a, b) {
a = await doubleAfterlSec(a);
b = await doubleAfterlSec(b);
return a + b;
}

// 用法
doubleAndAdd(1, 2).then(console.log);

function doubleAfterlSec(param) {
return new Promise (resolve => {
setTimeout(resolve(param * 2), 1000);
});
}
  • 6.2 并行调用 async/await

在前面的例子中,我们调用了两次 await,每次调用都会等待一秒(总共两秒)。不过既然 ab 没有相互依赖,我们可以使用 Promise.all 来并行调用它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 异步函数本身返回一个Promise
async function doubleAndAdd(a, b) {
//注意:这边使用 Promise.all
//注意到使用数组解构,捕获结果
[a, b] = await Promise.all([doubleAfterlSec(a), doubleAfterlSec(b)]);
return a + b;
}

// 用法
doubleAndAdd(1, 2).then(console.log);

function doubleAfterlSec(param) {
return new Promise (resolve => {
setTimeout(resolve(param * 2), 1000);
});
}
  • 6.3 async/await 函数时的错误处理

使用 async await 时有好几种处理错误的方式。

Option 1 - 在函数中使用 try catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 使用 try catch

async function doubleAndAdd(a, b) {
try {
a = await doubleAfterlSec(a);
b = await doubleAfterlSec(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 doubleAfterlSec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) > reject(NaN) : resolve(val);
}, 1000);
});
}

Option 2 — catch 每一个 await

由于每个 await 表达式都会返回一个 Promise,因此您可以在每行上捕获错误,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Option 2 - *Catch* 每个等待线上的错误
// 因为每个等待表达本身就是一个Promise
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);
});
}

Option 3 — catch 整个 async-await 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Option 3 - 不要做任何事情,在function外捕获
// 因为异步/等待返回一个Promise,我们可以捕捉整个函数的错误
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);
});
}

ECMAScript 2018

ECMAScript 目前处于最终草案中,将于 2018 年 6 月或 7 月发布。以下所有特性均在 Stage-4 中,并将成为 ECMAScript 2018 的一部分。

这是一个非常先进重大特性,是对 JS 引擎的核心增强。

主要的想法是为了给 JavaScript 带来一些多线程的特性,允许内存不仅仅由 JS 引擎来管理,而是由 JS 开发者们自行管理,以便将来能够编写高性能、可并发的程序。

这个特性由一个新的全局对象 SharedArrayBuffer 实现,它将数据存储在一块共享内存空间中。所以这些数据可以在 JS 主线程和 web-worker 线程之间共享。

到现在为止,如果我们想要在 JS 主线程和 web-worker 之间共享数据,我们必须复制数据并使用 postMessage 将其发送到另一个线程。没别的办法!

您只需使用 SharedArrayBuffer,数据立马就能同时被主线程和多个 web-worker 线程访问到。

但在线程之间共享内存可能会导致竞争状况。为了避免竞争条件,又引入了全局对象 Atomics(原子性)。Atomics 提供了一些方法,用来在一个线程使用共享内存的数据时给共享内存加锁。它还提供了方法来安全地更新共享内存中的(上锁的)数据。

建议通过某个库使用此特性,不过目前没有构建在此功能之上的库。

如果您有兴趣,推荐阅读:

  1. From Workers to Shared Memory — lucasfcosta
  2. A cartoon intro to SharedArrayBuffers — Lin Clark
  3. Shared memory and atomics — Dr. Axel Rauschmayer
  • 2. 移除了带标签的模板字符串的限制

首先,我们需要清楚“带标签的模板字符串(Tagged Template literal)”是什么,这样我们可以更好地理解这个特性。

Tagged template literal 是 ES2015+ 中的特性,它允许开发者自定义如何修改字符串。例如,以标准方式插入字符串,如下所示

1
2
3
4
// 标准字符串插值
const firstName = 'Raja';
const greetings = `Hello ${firstName}!`;
console.log(greetings); // "Hello Raja!"

在 tagged template literal 中,你可以编写一个自定义函数(例如greet),它接收字符串文字的硬编码部分(比如 ['Hello','!'])以及要替换的变量(例如['Raja'])作为参数,返回值可以是任何东西,由你决定。

下面的例子展示了我们的自定义“Tag”函数 greet 根据当前时间为模板字符串添加相应的问候语。

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
// "标记”函数返回一个自定义字符串文字.
// 在这个例子中,greet调用timeGreet()来追加Good
// Morning/Afternoon/Evening 视当天的时间而定.
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}!`; //👈🏼<-- 标记文字
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!';
}

现在我们已经讨论了“标签”函数是什么,许多人想要在不同的域中使用这个功能,比如用于终端命令,或者拼装 HTTP 请求的 URI 等等。

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

问题是ES2015和ES2016规范不允许使用“\u”(unicode),“\x”(十六进制)等转义字符,除非它们看起来完全像\u00A9\uA9A\xA9

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

在ES2018中,只要标记函数返回具有“cooked”属性(其中无效字符为“undefined”)的对象中的值,然后将“raw”属性(与任何你想要的)。

1
2
3
4
5
6
7
8

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

var str = myTagFunc `hi \ubla123abla`; //调用 myTagFunc

str // { cooked: "undefined", raw: "hi \\unicode" }
  • 3. 正则表达式的“.”标识

目前在正则中,虽然点号(“.”)应该匹配任一单个字符,但其实它无法匹配换行符,如 \n \r \f 等等。

比如:

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

这项改进使点号可以匹配任何单个字符。为了确保这不会破坏任何内容,我们需要给正则表达式加上 \s 标识它才会生效。

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

以下是提案文档中的 API:

1
2
3
4
5
6
const re = /foo.bar/s;
// 或者 const re = new RegExp('foo.bar', 's');

re.test('f00\nbar'); //true
re.dotAll // true
re.flags // 's'
  • 4. 正则表达式命名组捕获(RegExp Named Group Captures 🔥)

这种增强功能是从其他语言(如Python,Java等)引入的有用特性。称为“命名组”。该特性允许开发者以 (?<name> ...) 的形式为正则表达式中的分组进行命名。然后,他们可以使用该名称轻松获取他们需要的任何分组。

  • 4.1 基本命名组的例子

在下面的示例中,我们使用 (?<year>) (?<month>) 和 (?day) 名称对一个日期正则的不同部分进行分组。结果将会是一个包含 groups 的对象,其值又是一个包含 yearmonthday 属性的对象,它们的值分别是对应的分组捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 之前
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", groups: undefined]

// 以后(ES2018)
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
  • 4.2 在正则表达式本身内使用命名组

我们可以使用 \k<group name> 格式在正则表达式本身中反向引用该组。以下示例展示了它的用法。

1
2
3
4
5
6
7
8
//下面例子,有个组叫“fruit”
//它既能匹配“apple”又能匹配“orange”.我们可以使用“\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
  • 4.3 在 String.prototype.replace 中使用命名组

现在,String 的 replace 实例方法也包含了命名分组的特性。所以我们可以很容易地交换字符串中的单词。

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

1
2
3
4
5
// 更换“firstName, lastName” ==> “lastName, firstName”

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

'Raja Rao'.replace(re, '$<lastName>, $<firstName>'); // "Rao, Raja"
  • 5. 对象的其余属性(Rest properties for Objects)

rest 操作符 ...(三个点)允许我们提取尚未提取的对象属性。

  • 5.1 你可以使用 rest 来帮助只提取你想要的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
// 提取 firstName 和 age
// 将其余的项存储在“remaining”变量中。

let { fristName, age, ...remaining } = {
fristName: 'john',
lastName: 'smith',
age: 20,
height: '5.10',
race: 'martian',
};
firstName; //john
age //20
remaining //{ lastName: 'smith', height: '5.10', race: 'martian' }
  • 5.2 更好的是,你可以删除不需要的!🔥🔥

1
2
3
4
5
6
7
8
9
10
11
//假设我们想要移除SSN,我们不需要遍历整个对象并创建一个新的cleanObj。
// 简单地使用rest解构来提取出SSN,并将其余的保存在cleanObj中。

let { SSN, ...cleanObj } = {
firstName: 'john',
lastName: 'smith',
SSN: '123-45-6789',
race: 'martian',
}

cleanObj; //{ firstName: 'john', lastName: 'smith', race: 'martian' }
  • 6. 展开对象属性 (Spread properties for Objects)

展开属性看起来就像具有三个点的 Rest 属性一样 ..., 但区别在于您使用展开来创建(重新构建)新对象。

提示:展开操作符用于等号的右侧。Rest 用于等号的左侧。

1
2
3
4
5
6
7
// person 和 account 合并
const person = { fName: 'john', age: 20 };
const account = { name: 'bofa', amount: '$1000' };

// 通过扩展运算符提取person和account的属性,并将其添加到一个新对象中。
const personAndAccount = { ...person, ...account };
personAndAccount; // {fName: 'john', age: 20, name: 'bofa', amount: '$1000' }
  • 7. 正则表达式的后行断言(零宽后发断言)

这是对正则表达式的增强,它允许我们确保某些字符串出现在其他字符串之前

您现在可以使用一个组 (?<=...)(问号,小于,等于)来查找肯定正断言。

此外,您可以使用 (?<!...)(问号,小于,感叹号)来查看否负断言。从本质上讲,只要断言通过,就会匹配。

正断言:假设我们想要确保 符号在单词 winning 之前存在(即:#winning)并且希望正则表达式只返回字符串“winning”。你可以这样写。

1
2
3
4
5
6
7
8
/(?<=#).*/.test('winning'); // false
/(?<=#).*/.test('#winning'); // true

//之前
'#winning'.match(/#.*/)[0]; // '#wining',包含#

//ES2018后
'#winning'.match(/(?<=#).*/)[0]; // 'wining' 没有#, #是用来验证的。

负断言:假设我们想提取前面是 € 符号而不是 $ 符号的数字。

1
2
3
4
5
6
7
// 3.00不能匹配,因为符号$在前面

'A gallon of milk is $3.00'.match(/(?<\$)\d+\.?\d+/); // null

// 2.43匹配成功,因为前面没有符号$
//当匹配时,不包含符号€
'A gallon of milk is €3.00'.match(/(?<\$)\d+\.?\d+/)[0]; //2.43

要编写正则表达式来匹配各种 Unicode 字符并不容易。像 \w\W\d 等只能匹配英文字符和数字。但是对于印度语,希腊语等其他语言的数字呢?

这就是 Unicode Property Escapes 的用武之地。结果是,Unicode 为每个符号(字符)添加了元数据属性,并使用它来对各种符号进行分组或描述各种符号。

例如,Unicode 数据库将所有印地语字符(हिन्दी)放在一个值为 DevanagariScript 属性,和一个具有相同值 DevanagariScript_Extensions的属性中。所以我们可以搜索 Script=Devanagari 得到所有印地文字符。

Devanagari(梵文)可用于马拉地语,北印度语,梵语等各种印度语言。

从 ECMAScript 2018 开始,我们可以使用 \p 转义字符和 {Script=Devanagari} 来匹配所有印度字符。也就是说,我们可以在正则表达式中使用:\p{Script=Devanagari} 来匹配所有梵文字符。

1
2
3
//他跟随匹配多个印地文字符
/^\p{Script=Devanagari}+$/u.test('हिन्दी'); //true
//PS:有3个印地文字符h

类似地,Unicode 数据库中希腊字符的 Script_Extensions(和Script)值为Greek。所以我们可以使用 Script_Extensions=GreekScript=Greek 来搜索所有希腊字符。

也就是说,我们可以在正则表达式中使用:\p{Script = Greek} 来匹配所有希腊字符。

1
2
//以下匹配一个希腊字符
/\p{Script_Extensions=Greek}/u.test('π'); // true

此外,Unicode 数据库将各种类型的 Emoji 存储在属性值为“true”的布尔属性 EmojiEmoji_ComponentEmoji_PresentationEmoji_ModifierEmoji_Modifier_Base 下。所以我们可以通过简单地使用 Emoji=true 来查找所有表情符号。

也就是说,我们可以使用:\p{Emoji}\Emoji_Modifier 等来匹配各种 Emoji。

下面的例子将会清楚。

1
2
3
4
5
6
7
8
9
10
11
12
// 下面是匹配表情字符
/\p{Emoji}/u.test('❤️'); //true
// 下面的操作失败了,因为黄色的emojis没有Emoji_Modifier!
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌️'); //false
//下面的操作是匹配的,\p{Emoji} 紧跟后面是 \p{Emoji_Modifier}
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌🏽'); //true
// 解释:
// 默认情况下,胜利表情是黄色的。如果我们使用相同表情的棕色、黑色或其他,它们被认为是原始表情的变体,并使用两个unicode字符表示。一个是原始表情符号,另一个是颜色的unicode字符。在下面的例子中,虽然我们只看到一个棕色的胜利表情符号,但它实际上使用了两个unicode字符,一个用于表情符号,另一个用于棕色。

//在Unicode数据库中,这些颜色具有Emoji_Modifier属性。因此,我们需要使用\p{Emoji}和\p{Emoji_Modifier}来正确和完全匹配棕色的表情符号。

/\p{Emoji}\p{Emoji_Modifier}/u.test('✌🏽'); //true

最后,我们可以使用大写的“P”(\P)转义字符而不是小 p(\p)来否定匹配。

参考:

  1. ECMAScript 2018 Proposal
  2. https://mathiasbynens.be/notes/es-unicode-property-escapes
  • 9. Promise.prototype.finally()

finally() 是 Promise 新增的实例方法。他的主要想法是无论 resolve 或是 reject 后都执行一个回调函数以帮助清理事情。finally 回调被调用时不传入参数,并且无论如何总是被执行。

我们来看看各种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Resolve

let started = true;
let myPromise = new Promise(function(resolve, reject) {
resolve('all good');
})
.then(val => {
console.log(val); //all good
})
.catch(e => {
console.log(e)l // 跳过
})
.finally(() => {
console.log('This function is always executed!');
started = false; // 清除
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Reject
let started = true;
let myPromise = new Promise(function(resolve, reject) {
resolve('reject apple');
})
.then(val => {
console.log(val); //reject apple
})
.catch(e => {
console.log(e)l //catch被跳过
})
.finally(() => {
//注意这里没有任何值
console.log('This function is always executed!');
started = false; //清除
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//错误情况1
//Promise 抛出错误


let started = true;
let myPromise = new Promise(function(resolve, reject) {
throw new Error('Error');
})
.then(val => {
console.log(val); // 跳过
})
.catch(e => {
console.log(e)l // 出现错误catch被调用
})
.finally(() => {
// 注意这里没有任何值
console.log('This function is always executed!');
started = false; // 清除
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

// 错误从“catch”事件中抛出

let started = true;
let myPromise = new Promise(function(resolve, reject) {
throw new Error('something happend');
})
.then(val => {
console.log(val); // 跳过
})
.catch(e => {
throw new Error('throw another error')
})
.finally(() => {
// 注意这里没有任何值
console.log('This function is always executed!');
started = false; //清除
// 错误形式的捕获将需要在其他地方调用函数来处理
})
  • 10. 异步迭代 (Asynchronous Iteration)

这是一个*非常*有用的特性。基本上它允许我们轻松创建异步代码循环!

这个特性增加了一个新的“for-await-of”循环,允许我们循环调用返回 Promise(或 Promise 数组)的异步函数。很酷的一点是,循环会等待每个 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
24
25
26
const promises = [
new Promise(resolve => resolve(1)),
new Promise(resolve => resolve(2)),
new Promise(resolve => resolve(3)),
]
// 之前
// for-of使用常规同步迭代器
// 不等待Promise 去解决

async function test1() {
for (const obj of promise) {
console.log(obj) // 记录3 条Promise对象
}
}

// 以后
// for-wait-of使用异步迭代

async function test2() {
for await (const obj of promises) {
console.log(obj); // 1,2, 3
}
}

test1() // promise, promise, promise
test2() // 1,2,3

基本上就是这些了!