ES9

2022/4/23 20:09 Javascript 大约 11 分钟

ECMAScript 9(简称ES9)是于2018年6月正式发布的JavaScript语言的标准,正式名为ECMAScript 2018(ES2018)。

# Promise.prototype.finally

方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在then()和catch()中各写一次的情况。

语法:

p.finally(onFinally);

p.finally(function() {
   // 返回状态为(resolved 或 rejected)
});
1
2
3
4
5

参数:

  • onFinally:Promise 结束后调用的Function。

返回值:返回一个设置了 finally 回调函数的Promise对象。

finally() 虽然与 .then(onFinally, onFinally) 类似,它们不同的是:

  • 调用内联函数时,不需要多次声明该函数或为该函数创建一个变量保存它。
  • 由于无法知道promise的最终状态,所以finally的回调函数中不接收任何参数,它仅用于无论最终结果如何都要执行的情况。
Promise.resolve()
    .then(result => {
        console.log('我执行了')
    })
    .catch(error => {})
    .finally(() => {
        console.log('我会始终执行')
    });
1
2
3
4
5
6
7
8

相关信息

在finally回调中 throw(或返回被拒绝的promise)将以 throw() 指定的原因拒绝新的promise.

# Asynchronous Iteration

此特性添加了一个新的“for-await-of”循环,允许我们在循环中调用返回 promises(或带有一堆 promise 的 Arrays )的异步函数。 循环会等待每个 Promise 在进行下一个循环之前 resolve 。

const promises = [
  new Promise(resolve => {
    setTimeout(() => {
      console.log(1)
      resolve(1)
    }, 5000)
  }),
  new Promise(resolve => {
    setTimeout(() => {
      console.log(2)
      resolve(2)
    }, 3000)
  }),
  new Promise(resolve => {
    setTimeout(() => {
      console.log(3)
      resolve(3)
    }, 3000)
  }),
]

async function test2() {
  // await Promise.all(promises)
  // console.log('end2')
  for await (const res of promises) {
    console.log(res, '--')
  }
}

test2();

console.log('end')

/**
 * end
 * 2
 * 3
 * 1
 * 1 --
 * 2 --
 * 3 --
 */
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

在这里我其实有一个疑问,for-await-of确实可以让当前结果resole之后,再执行下一个循环,但是我没发现这和promise.all有什么区别。

# RegExp Unicode Property Escapes

了解这个新的知识点,需要对文本的编码非常熟悉,不然意识不到这个功能的意义。对于文本的编码需要了解两个概念:字符编码和文件编码。

  • 字符编码包括 ASCII 和 Unicode
  • 文件编码包括 UTF-8、GBK等。

文件编码和字符编码没有关系,也就是说即使指定了文件编码,字符变也可以灵活选择而不受任何限制。根据 Unicode 规范,每一个 Unicode 字符除了有唯一的码点,还具有其它属性:

  • Unicode Property
  • Unicode Script
  • Unicode Block

这些一般用在正则里面。

# Unicode Property

过去ECMAScript 的原生RegExp 无法直接存取有些Unicode 的字元property,简化来说就是原生RegExp 无法完整支援Unicode。因为原生未提供,所以只能透过其他library 来解决,但这些方法不是很理想。ES9提供了RegExp 的Unicode property escapes,语法为 \p{...} 和\P{...},而其中的 p 代表的是「property」的意思。Unicode property escapes 是一种新的escape sequence (跳脱序列),可用于设定uflag 的RegExp,这样可以更好的表示 \p{...} 内的表达式与Unicode property 有关。此提案解决了过去的问题,因为已变为原生支援,无须使用额外的library。\P{...}是 \p{...} 的反向形式(negated form),就跟 \d 代表数字字元,\D代表非数字字元一样,大写字有「negated」的意思。

Unicode Property按照字符的功能对字符进行分类,一个字符只能属于一个Unicode Property。也就是说 Property 并不关心字符所属的语言,只关心字符的功能。

可以将Unicode property 理解为字符组,将小写 p 改成大写,就是该字符组的排除型字符组。想想看 \d 匹配 0-9 这个字符组,而 \D 匹配 0-9 以外的字符组。

let input = 'abcdAeCd中国'

console.log(input.match(/\p{L}/ug)); // ["a", "b", "c", "d", "A", "e", "C", "d", "中", "国"]
1
2
3

这段代码的含义是在输入中匹配所有的字符(不限语言),这里使用的是 Unicode Property:{L},这个属性的含义是任何语言的任何字母。

# Unicode Script

按照字符所属的书写系统来划分字符,它一般对应某种语言。比如 \p{Script=Greek} 表示希腊语,\p{Script=Han} 表示汉语。

// 匹配下列字符串中的中文
let input = `I'm chinese!我是中国人`

console.log(input.match(/\p{Script=Han}+/u)); // [ '我是中国人', index: 12, input: "I'm chinese!我是中国人", groups: undefined ]

// es9之前
console.log(input.match(/[\u4e00-\u9fa5]+/))

// π 字符是一个希腊字符,通过指定 \p{Script=Greek} 就可以匹配这个字符了
const regexGreekSymbol = /\p{Script=Greek}/u;
console.log(regexGreekSymbol.test('π')); // true

1
2
3
4
5
6
7
8
9
10
11
12

相关信息

  1. \u4e00和\u9fa5是unicode编码,并且正好是中文编码的开始和结束的两个值,所以可以使用这个正则表达式来判断字符串中是否包含中文。
  2. 虽然不同的写法看上去结果一样,然而时光飞逝,Unicode 在2017年6月发布了10.0.0版本。在这20年间,Unicode 添加了许多汉字。比如 Unicode 8.0 添加的 109 号化学元素「鿏(⿰⻐麦)」,其码点是 9FCF,不在这个正则表达式范围中。而如果我们期望程序里的/[\u4e00-\u9fa5]/可以与时俱进匹配最新的 Unicode 标准,显然是不现实的事情。现在只需要在 Unicode Scripts 找到对应的名称即可,而不需要自己去计算所有对应语言字符的的 Unicode 范围。
  3. 虽然可以通过引用希腊字符(或者其他编码)表做正则处理,当每当更新表时,更新起来会非常麻烦,不如让浏览器原生支持 \p{UnicodePropertyName=UnicodePropertyValue} 的正则语法,帮助开发人员解决这个烦恼。

# Unicode Block

将 Unicode 字符按照编码区间进行划分,所以每一个字符都只属于一个 Unicode Block。

示例:

  • \p{InBasic_Latin}: U+0000–U+007F

  • \p{InLatin-1_Supplement}: U+0080–U+00FF

  • \p{InLatin_Extended-A}: U+0100–U+017F

  • \p{InLatin_Extended-B}: U+0180–U+024F

::: warnign 目前 JavaScript RegExp 还不支持 Unicode Block :::

# RegExp Lookbehind Assertions

这个是对正则表达式的增强。javascript 正则表达式一直不支持后行断言,后向断言会获取某个字符后面跟的内容,在获取美刀等货币单位上有很大用途。

# 前行断言

// 它匹配字符串'aabb',但整体匹配的字符串不包括 b。
const RE_AS_BS = /aa(?=bb)/;
const match1 = RE_AS_BS.exec('aabb');
console.log(match1[0]); // 'aa'

// 此外,它不匹配没有两个 b 的字符串
const match2 = RE_AS_BS.exec('aab');
console.log(match2); // null

// 否定的前行断言
const RE_AS_NO_BS = /aa(?!bb)/;
RE_AS_NO_BS.test('aabb'); // false
RE_AS_NO_BS.test('aab'); // true
RE_AS_NO_BS.test('aac'); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 后行断言

// 'foo'仅当它前面有美元符号时才被替换。
const RE_DOLLAR_PREFIX = /(?<=\$)foo/g;
const str = '$foo %foo foo'.replace(RE_DOLLAR_PREFIX, 'bar');
console.log(str); // $bar %foo foo

// 没有后向断言的情况下实现相同的结果
const RE_DOLLAR_PREFIX = /(\$)foo/g;
const str = '$foo %foo foo'.replace(RE_DOLLAR_PREFIX, '$1bar');
console.log(str); // $bar NaNoo foo

// 否定的后行断言
const RE_NO_DOLLAR_PREFIX = /(?<!\$)foo/g;
const str = '$foo %foo foo'.replace(RE_NO_DOLLAR_PREFIX, 'bar');
console.log(str); // $foo %bar bar
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Rest/Spread Properties

Rest/Spread在ES6中已经引入,不过es6中只针对于数组。在ES9中为对象提供了像数组一样的REST参数和扩展运算符。

Sebastian Markbåge 提出的 Rest/Spread Properties 提案包括两部分:

  • 用于对象解构的 rest 操作符(...)。目前,这个操作符只能在数组解构和参数定义中使用。
  • 对象字面量中的 spread 操作符(...)。目前,这个操作符只能用于数组字面量和在函数方法中使用。

# Spread(展开语法)

语法(Spread syntax, 可以在使用数组类型时) 将表达式展开在函数表达式中;还可以在构造字面对象时,按key-value的方式展开。

语法:

  • 函数调用
    myFunction(...iterableObj);
    
    1
  • 字面量字体或字符串
    [...iterableObj, '4', ...'hello', 6];
    
    1
  • 构造字面量对象或属性复制时
    let objClone = { ...obj };
    
    1
// 在函数调用时使用展开语法
function myFunction(x, y, z) { }
var args = [0, 1, 2];
myFunction(...args);

// 构造字面量时
var parts = ['shoulders', 'knees'];
var lyrics = ['head', ...parts, 'and', 'toes'];
var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }

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

相关信息

  • 展开运算符属于浅拷贝。
  • Object.assign()函数会触发 setters,而展开语法则不会。
  • 不能替换或模拟 Object.assign()函数:
var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };
const merge = ( ...objects ) => ( { ...objects } );

var mergedObj = merge ( obj1, obj2);
// Object { 0: { foo: 'bar', x: 42 }, 1: { foo: 'baz', y: 13 } }

var mergedObj = merge ( {}, obj1, obj2);
// Object { 0: {}, 1: { foo: 'bar', x: 42 }, 2: { foo: 'baz', y: 13 } }
1
2
3
4
5
6
7
8
9

# rest(剩余参数语法)

该语法允许我们将一个不定数量的参数表示为一个数组。如果函数的最后一个命名参数以...为前缀,则它将成为一个由剩余参数组成的真数组,其中从0(包括)到theArgs.length(排除)的元素由传递给函数的实际参数提供。

语法:

function(a, b, ...theArgs) {
  // ...
}
1
2
3
function main(a, ...rest) {
    console.log(a); // 1
    console.log(rest); // [2, 3]
}

main(1, 2, 3)
1
2
3
4
5
6

相关信息

剩余参数和 arguments对象之间的区别主要有三个:

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。
  • arguments对象不是一个真正的数组,而剩余参数是真正的 Array实例,也就是说你能够在它上面直接使用所有的数组方法,比如 sort,map,forEach或pop。
  • arguments对象还有一些附加的属性 (如callee属性)。

注意

在每个对象字面量的顶层,可以使用 rest 操作符最多一次,并且必须只能在末尾出现:

const {...rest, foo} = obj; // SyntaxError
const {foo, ...rest1, ...rest2} = obj; // SyntaxError
1
2

# RegExp named capture groups

可以使用(?...)语法为捕获组指定名称name。日期的正则表达式可以写成/(?\d{4})-(?\d{2})-(?\d{2})/u。每个名称都应该是唯一的,并遵循 ECMAScript IdentifierName 的语法。可以从groups正则表达式结果的属性的属性中访问命名组。与未命名的组一样,也会创建对组的编号引用。

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

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

// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

// result[0] === '2015-01-02';
// result[1] === '2015';
// result[2] === '01';
// result[3] === '02';
        
// 解构获取组名
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
console.log(`one: ${one}, two: ${two}`);  // one: foo, two: bar
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

# s (dotAll) flag for regular expressions

在正则表达式模式中,我们可以使用点 . 匹配任意的单个字符。但是这里面却有两个例外,默认:

  • . 不匹配 星体字符
  • . 不匹配 行终止符

默认不匹配 星体字符,我们可以通过给正则设置 u (unicode) 这个标志来解决这个问题。但是对于 行终止符 来说,却没有一个类似的 flag 来解决这个问题。按照正常的正则语义,点 . 可以匹配任意字符,但实际上只能识别下面列出的 行终止符 :

  • U+000A 换行 (LF) (\n)
  • U+000D 回车 (CR) (\r)
  • U+2028 线分隔符
  • U+2029 段落分隔符

在实际的场景中,还有一些字符可以被认为是 行终止符 ,比如说:

  • U+000B 垂直制表符 (\v)
  • U+000C 换页 (\f)
  • U+0085 下一行

所以 es9 新增了一个 flag s,来补充上面的场景。

const str = `hello\nworld`;
const r1 = /hello.world/;
console.log(r1.test(str), r1.dotAll, r1.flags); // false false

// 添加 's' flag
const r2 = /hello.world/s;
console.log(r2.test(str), r2.dotAll, r2.flags); // true true s
1
2
3
4
5
6
7

# Lifting template literal restriction

放松对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串。

Template literals遵从字符串的转义规则:

  • \u开头,后跟4个16进制数字,例如,\u00B1表示±
  • \u开头,使用大括号括起来的16进制数字,例如,\u{2F804}表示你
  • \x开头,后跟2个16进制数字,例如,\xB1表示±
  • \开头,后跟10进制数字,用来表示八进制字面量(注:strict mode下不支持)

解释器遇到\u和\x,如果发现后面的字符不满足以上条件,就会报语法错。

const str = `\unicode`;

console.log(str, 'Yahweh'); //  Invalid Unicode escape sequence
1
2
3

Lifting template literal restriction改善了这个问题:

  • 去除\u或者\x的限制,只适用于Tagged template literals,而对于untagged template literals无效,仍然会报语法错。
  • 为了兼容性,该提案只是将转义后的字符串数组strs,置为单元素数组[undefined],tag函数需要手动处理strs.raw进行处理。
const foo = (strs, ...exprs) => {
    console.log(strs);        // [undefined]
    console.log(strs.raw);    // ["something else \unicode"]
};

foo`something else \unicode`;
1
2
3
4
5
6

上面代码中,模板字符串原本是应该报错的,但是由于放松了对字符串转义的限制,所以不报错了,JavaScript 引擎将第一个字符设置为undefined,但是raw属性依然可以得到原始字符串,因此tag函数还是可以对原字符串进行处理。

# 参考博文

上次编辑于: 2023年7月4日 09:36