ES2022有什么新特性?

Posted by Dan on December 10, 2021

ES2022是什么?它的新特性又是什么呢?

ES2022是即将在2022年发布的JavaScript新特性。

ES2022的新特性有哪些?是谁来决定要发布哪些特性呢?这些特性又是谁发布的?

想要了解接着往下看,首先要介绍一下ECMA和TC39

ECMA & TC39

ECMA是一个国际化的组织,像ISO,IETE,W3C这种。

TC39是ECMA International标准化组织旗下的技术委员会的一员,负责管理着ECMAScript语言和标准化API

ECMAScript的缩写就是ES,我们熟悉的ES6,ES7,ES8等就是这个缩写加版本号来的。

ECMAScript语言和标准化API又可以分为两个标准:

  • 第一个是ECMA-262标准,它包含了语言的语法和核心的API
  • 另一个标准ECMA-402则包含一些国际化的API,提供给ECMAScript核心API选择性支持。

TC39

TC39成员的组成是由一些大型网站、学术研究者以及OpenJS基金会。以及社区代表如Babel、nodejs社区。还有一些突出贡献者,对某一议题有特殊贡献的人。

委员会成员每两个月开会讨论一次提案,由于疫情会在线上举办。还有GitHub和论坛,GitHub是完成大部分开发和决定的地方,论坛则是提案更早期讨论发起的地方。

开会主要形式是讨论,目的是为了达成一致,没有投票。

由于委员会由多样化的人组成,代表了不同的利益群体。TC39委员会的工作就是满足大家的需求和目标。

TC39的5个阶段

从想法到最终应用提案要经历5个阶段

  • 0 Strawman稻草人: 仅有一个想法
  • 1 Proposal提案:向委员会讲解和介绍概述解决方案,提出潜在困难。到此只是表明是一个值得讨论的议题且愿意继续讨论。
  • 2 Draft草案:需讨论具体的语法语义的细节。提供具体的解决方法。
  • 3 Candidate候选:需要接收具体实现者和用户们的反馈。
  • 4 finished结束:新特性通过具体的测试,代表可以被大家使用。提案的标准和规范也会进入到主要的标准规范中。

近年来的版本

全称 发布年份 缩写 / 简称
ECMAScript 2015 2015 ES2015 / ES6
ECMAScript 2016 2016 ES2016 / ES7
ECMAScript 2017 2017 ES2017 / ES8
ECMAScript 2018 2018 ES2018 / ES9
ECMAScript 2019 2019 ES2019 / ES10
ECMAScript 2020 2020 ES2020 / ES11
ECMAScript 2021 2021 ES2021 / ES12
ECMAScript 2022 2022 ES2022 / ES13

从ES2015开始往后的每一年发布一个版本,更新还是很频繁的。

ES2022特性

首先看下都有哪些新特性:

  • Class Fields
  • RegExp Math Indices
  • Top-level await
  • Ergonomic brand checks for Private Fields
  • .at()
  • Object.hasOwn()
  • Class Static Block

接下来我会一一介绍,如何使用这些新特性。

请打开浏览器控制台(Mac快捷键: cmd+option+i),如下示例都可以在Chrome浏览器控制台执行。

Class Fields

在ES6提出class类之后,类仅支持公有变量,如果想要定义私有变量,约定在属性前面加下划线,但是此变量依然可以从外部访问到。如下ES6示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ButtonToggle {
  constructor(){
    // 公共属性
    this.color = 'green'
    // 定义私有属性
    this._value = true
  }

  toggle() {
    this.value = !this.value
  }

  static startFun () {
    console.log('static 方法');
  }
}

调用访问得到的结果如下,所以之前是没有私有变量的。

1
2
3
4
5
6
7
8
9
const button = new ButtonToggle();
console.log(button.color); // green

// 私有变量也可以访问
button._value = false;
console.log(button._value); // false

// 访问静态方法
ButtonToggle.startFun(); // static 方法

而在ES2022中提案版本中,包括了私有变量、私有方法、静态私有变量、静态私有方法四个,都安排上了。

设置私有属性的方法是在变量前面加上一个修饰符#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ButtonDemo {
  // 公有变量
  color = 'green';
  // 私有变量
  #value = true;
  // 静态私有变量
  static #private_static_field = 2

  // 私有方法
  #privateMethod () {
    console.log('this is a private method');
  }

  // 静态私有方法
  static #toggle() {
    console.log('hello toggle');
  }

  publicFunc () {
    this.#privateMethod();
    console.log(ButtonDemo.#private_static_field, this.#value); 
    ButtonDemo.#toggle();
  }
}

RegExp Math Indices

以前的正则表达式后面可以加ig去修饰。i代表忽略大小写,g代表全局匹配,现在又增加了d

在正则后面添加d匹配,则会返回匹配字符串的索引位置,通过indices字段拿到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const re1 = /a+(?<Z>z)?/d;

// indices are relative to start of the input string:
const s1 = "xaaaz";
const m1 = re1.exec(s1);
m1.indices[0][0] === 1;
m1.indices[0][1] === 5;
console.log(s1.slice(...m1.indices[0]) === "aaaz"); // true

m1.indices[1][0] === 4;
m1.indices[1][1] === 5;
console.log(s1.slice(...m1.indices[1]) === "z"); // true

m1.indices.groups["Z"][0] === 4;
m1.indices.groups["Z"][1] === 5;
console.log(s1.slice(...m1.indices.groups["Z"]) === "z"); // true

// capture groups that are not matched return `undefined`:
const m2 = re1.exec("xaaay");
console.log(m2.indices[1] === undefined); // true
console.log(m2.indices.groups["Z"] === undefined); // true

控制台打印m1如下:

img

仔细查看发现m1下面多了一个indices这个属性,里面返回是是匹配到的字符的索引,是一个数组。

是否还注意到有个groups,返回groups是因为正则里面包含了(?<Z>z),这里的意思是,如果匹配到z就把匹配到的字符放到z属性里,具体查看Groups and ranges

Top-level await

顶层await。以前我们使用await必须要和async配合一起使用。

现在await将不受async的限制,如下在全局作用域使用import的异步加载方式。

1
2
3
4
5
6
let jQuery;
try {
  jQuery = await import('https://cdn-a.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.com/jQuery');
}

还可以这样使用,是不是很方便。

1
const connection = await dbConnector();

Ergonomic brand checks for Private Fields

支持使用in去判断私有属性在对象里面存不存在

1
2
3
4
5
6
7
8
9
10
11
12
class C {
  #brand;

  static isC(obj) {
    try {
      obj.#brand;
      return true;
    } catch {
      return false;
    }
  }
}

如上按照之前的方式判断,也可以满足情况,但会非常笨拙。

使用in来解决此问题

1
2
3
4
5
6
7
8
9
10
11
class C {
  #brand;

  #method() {}

  get #getter() {}

  static isC(obj) {
    return #brand in obj && #method in obj && #getter in obj;
  }
}

.at()

取数组索引,传参为正正序取,传参为负从后向前取

1
2
3
4
var test = ['a', 'b', 'c']
test.at(1) // b
test.at(-1) // c
test.at(-2) // b

同样的也支持字符串,根据索引取值

1
2
3
4
var str = 'abc'
str.at(1) // b
str.at(-1) // c
str.at(-2) // b

Object.hasOwn()

提倡使用Object.hasOwn()这个方法,来代替Object.prototype.hasOwnProperty

在之前我们怎么判断一个对象呢?有两个小问题

Object.create(null)

Object.create(null)创建一个对象,但这个对象并不是继承Object.prototype,所以使用下面的方法判断会抛出异常。

1
2
Object.create(null).hasOwnProperty("foo")
// Uncaught TypeError: Object.create(...).hasOwnProperty is not a function

重定义hasOwnProperty()

使用时不确定是不是访问的是原生的属性,有可能是重定义的就会出现如下错误。

1
2
3
4
5
6
7
8
let object = {
  hasOwnProperty() {
    throw new Error("gotcha!")
  }
}

object.hasOwnProperty("foo")
// Uncaught Error: gotcha!

所以有了下面的方法,这种写法应该很普遍,来判断一个对象是不是它自己的。

1
2
3
4
5
let hasOwnProperty = Object.prototype.hasOwnProperty

if (hasOwnProperty.call(object, "foo")) {
  console.log("has property foo")
}

我们在自己项目中这么写,有很多库也出了这样的方法来判断,于是乎大家的需求,委员会就考虑安排上了。于是就有了hasOwn方法,让使用起来更简单。

1
2
3
if (Object.hasOwn(object, "foo")) {
  console.log("has property foo")
}

Class Static Block(类静态成员初始化块)

在此以前,我们初始化类的静态成员变量只能在定义的时候去做,不能放到构造函数里面。

现在可以在类的内部开辟一个专门为静态成员初始化的作用域,在一些比较复杂的场景很适用.

class static block语法

1
2
3
4
5
class C {
  static {
    // statements
  }
}

那一般会怎么用呢?如下在没有类静态成员初始化块之前,我们可能用一个工具函数去初始化静态成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// without static blocks:
class C {
  static x = ...;
  static y;
  static z;
}

try {
  const obj = doSomethingWith(C.x);
  C.y = obj.y
  C.z = obj.z;
}
catch {
  C.y = ...;
  C.z = ...;
}

使用静态块,是不是优雅了不少。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// with static blocks:
class C {
  static x = ...;
  static y;
  static z;
  static {
    try {
      const obj = doSomethingWith(this.x);
      this.y = obj.y;
      this.z = obj.z;
    }
    catch {
      this.y = ...;
      this.z = ...;
    }
  }
}

想要更好的理解,如下举个🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];
  static _ = initializeTranslator(); // (A)
}
// 初始化函数
function initializeTranslator() {
  for (const [english, german] of Object.entries(Translator.translations)) {
    Translator.englishWords.push(english);
    Translator.germanWords.push(german);
  }
}

一个类想要同时初始化创建两个静态变量,只能引入外部函数A行。

但这个代码还有两个问题

  • 调用initializeTranslator()这个函数是额外的步骤,必须要等到类初始化之后执行,得通过一个变通的方案来执行,又会引入一些其他代码。
  • 而且是initializeTranslator()函数内部,不能访问类的私有数据。

如果使用类静态初始化块,就会清晰很多.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];

  // 静态块
  static { // (A)
    // 静态成员初始化
    for (const [english, german] of Object.entries(this.translations)) {
      this.englishWords.push(english);
      this.germanWords.push(german);
    }
    console.log(this.englishWords, this.germanWords);
  }
}

还可以使用多个static block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
  static field1 = console.log('field1 called');
  static {
    console.log('Class static block #1 called');
  }
  static field2 = console.log('field2 called');
  static {
    console.log('Class static block #2 called');
  }
}

/*
> "field1 called"
> "Class static block #1 called"
> "field2 called"
> "Class static block #2 called"
*/

现有的面向对象语言Java、C#都有相应的方法供静态成员初始化。JavaScript中类添加类静态块,让类的静态特性更趋完善,使用起来也会更优雅方便。

以上就是所有新特性,欢迎补充交流。:)


Finished Proposals

TC39 GitHub

TC39官网

十分钟揭秘TC39: ES2020和ES2021

ES2022特性:类静态初始化块

Class static initialization blocks