ECMAScript 2022 预览:10 个在 2021 进入 Stage 4 的提案

来源: OSCHINA
编辑: 局长
2022-01-14

2021 年,多项提案进入了 TC39 的 Stage 4 阶段。按照 TC39 的运作流程,每个提案都从 Stage 0 开始,而进入 Stage 4 则意味着该提案已被 ECMAScript 编辑签署同意意见,成为了事实上的标准特性。

本文整理了在 2021 年进入 Stage 4 的 10 个提案,它们将会被吸纳进 ECMAScript 2022。


Class Fields

声明类的字段。

提案链接:https://github.com/tc39/proposal-class-fields

到目前为止,在 ES 规范中,类的字段定义和初始化是在类的构造函数中完成的。但是在新的提案中,类的字段可以在类的顶层被定义和初始化。

class Post {
 title;
 content;
 shares = 0;
}

提案为 ECMAScript Class 新增了下表中所描述的特性(绿色为现有特性):

提案所包含的特性目前已经在 Chrome 74,Node 12,Safari Technology Preview 117,TypeScript 3.8,Babel 7.0+ 等等环境中使用。不过,需要注意的是,因为如 TypeScript 在提案正式进入 Stage 4 之前就已经有各自的 Class 字段实现,所以在具体细节语义上会与先行 ECMAScript 标准有所差异。

class Base {
  name: string;
  constructor() {
    this.initProps();
  }

  initProps() {
    this.name = 'xxx';
  }
}
class Derived extends Base {
  age: number;

  initProps() {
    super.initProps();
    this.age = 10;
  }
}

const d = new Derived();
console.log(d.age);

Private Fields, Methods

该提案属于 Class Fields 系列提案的一部分,其使用#前缀定义类的私有方法和字段。

提案链接:https://github.com/tc39/proposal-private-methods

class Example {
  #value;

  constructor(value) {
    this.#value = value;
  }

  #calc() {
    return this.#value * 10;
  }

  print() {
    console.log(this.#calc());
  }
}

const object = new Example(5);
console.log(object.#value);    // SyntaxError
console.log(object.#calc());   // SyntaxError
object.print();                // 50

Public Static Class Fields

提案链接:https://github.com/tc39/proposal-static-class-features

在之前的类的字段和私有方法提案的基础上,该提案为 JavaScript 类增加了静态公共字段(Static public fields)、静态私有方法(Static private methods)和静态私有字段(Static private fields) 的特性。

// without static class fields:
class Customer {
  // ...
}
Customer.idCounter = 1;

// with static class fields:
class Customer {
  static idCounter = 1;
  // ...
}

Private Fields In Operator

检测私有字段是否存在。

提案链接:https://github.com/tc39/proposal-private-fields-in-in

由于尝试访问对象上不存在的私有字段会引发异常,因此需要能够检查对象是否具有给定的私有字段。

这个提案提供了使用 in 操作符来判断前不久正式进入 Stage 4 的 Class Private Fields 提案中引入的 字段 是否在一个对象中存在。相比于直接通过访问私有字段 try { obj.#foo } catch { /* #foo not exist in obj */ } 来判断一个对象是否有安装对应的 字段 来说,Private-In 可以区分是访问错误,还是真正没有  字段 ,如以下场景通过 try-catch 就无法区分是否是访问异常还是 字段 确实不存在:

class C {
  get #getter() { throw new Error('gotcha'); }
  
  static isC(obj) {
    try { 
      obj.#getter;
      return true;
    } catch {
      return false;
    }
  }
}

而通过与普通字段类似的 in 操作符语义可判断一个 #field 是否存在在一个对象上:

class C {
  #brand;

  #method() {}

  get #getter() {}

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

Class Static Initialization Blocks

提案链接: https://github.com/tc39/proposal-class-static-block

类静态初始化块 (Class Static Initialization Blocks) 提供了一种在类声明/定义期间评估静态初始化代码块的方式,可以访问类的私有字段 (Class Private Fields)。

自从有了 Class Private Fields,对于类的语法是不断地有新的实践与需求。这个提案提议的类静态初始化块会在类初始化时被执行。Java 等语言中也有类似的静态初始化代码块的能力,Static Initialization Blocks

提案中定义的初始化代码块可以获得 class 内的作用域,如同 class 的方法一样,也意味着可以访问类的 #字段。通过这个定义,我们就可以实现 JavaScript 中的 Friend 类了。

class Example {
  static propertyA;
  static #propertyB; // private

  static { // static initializer block
    try {
      const json = JSON.parse(fs.readFileSync('example.json', 'utf8'));
      this.propertyA = json.someProperty;
      this.#propertyB = json.anotherProperty;
    } catch (error) {
      this.propertyA = 'default1';
      this.#propertyB = 'default2';
    }
  }

  static print() {
    console.log(Example.propertyA);
    console.log(Example.#propertyB);
  }
}

Example.print();

Relative indexing .at() method

在所有内置的可索引数据上新增 .at() 方法。

提案链接:https://github.com/tc39/proposal-relative-indexing-method

该提案提供了一种从字符串(或数组)的开头(正向索引)或结尾(反向索引)获取元素的方法,无需使用临时变量。

很多时候,类似于 Python 中的数组负值索引非常实用。比如在 Python 中我们可以通过 arr[-1] 来访问数组中的最后一个元素,而不用通过目前 JavaScript 中的方式 arr[arr.length-1]来访问。这里的负数是作为从起始元素(即arr[0])开始的反向索引。

但是现在 JavaScript 中的问题是,[] 这个语法不仅仅只是在数组中使用(当然在 Python 中也不是),而在数组中也不仅仅只可以作为索引使用。像arr[1]一样通过索引引用一个值,事实上引用的是这个对象的 "1" 这个属性。所以 arr[-1] 已经完全可以在现在的 JavaScript 引擎中使用,只是它可能不是代表的我们想要表达的意思而已:它引用的是目标对象的 "-1" 这个属性,而不是一个反向索引。

这个提案提供了一个通用的方案,我们可以通过任意可索引的类型(Array,String,和 TypedArray)上的 .at 方法,来访问任意一个反向索引、或者是正向索引的元素。

// 数组
[0, 1, 2, 3, 4, 5].at(-1); // => 5
[0, 1, 2, 3, 4, 5].at(-2); // => 4

// 字符串
'abcdefghi'.at(-1); // => i
'abcdefghi'.at(-2); // => h

Object.hasOwn

提案链接:https://github.com/tc39/proposal-accessible-object-hasownproperty

简单来说,该提案就是使用Object.hasOwn替代Object.prototype.hasOwnProperty.call,是一种更简洁、更可靠地检查属性是否直接设置在对象上的方法。

const example = {
  property: '123'
};

console.log(Object.prototype.hasOwnProperty.call(example, 'property'));
console.log(Object.hasOwn(example, 'property')); // preferred

Error Cause

提案链接:https://github.com/tc39/proposal-error-cause

Error Cause 是阿里巴巴提出的提案,据称也是中国首个进入 Stage 4 的 TC39 提案。

此提案为 JavaScript 中的 Error 构造函数新增了一个属性 cause,开发者可以通过这个属性为抛出的错误附加错误原因,来清晰地跨越多个调用栈传递错误上下文信息。具体来说,该提案为 Error Constructor 新增了一个可选的参数 options,其中可以设置 cause 并且接受任意 JavaScript 值(JavaScript 可以 throw 任意值,如 undefined 或者字符串),将这个值赋值到新创建的 error.cause 上。

try {
  return await fetch('//unintelligible-url-a') // 抛出一个 low level 错误
      .catch(err => {
      throw new Error('Download raw resource failed', { cause: err }) // 将 low level 错误包装成一个 high level、易懂的错误
    })
} catch (err) {
  console.log(err)
  console.log('Caused by', err.cause)
  // Error: Download raw resource failed
  // Caused by TypeError: Failed to fetch
}

RegExp Match Indices ('d' Flag)

提案链接:https://github.com/tc39/proposal-regexp-match-indices

当前 ECMAScript 中的 RegExp.prototype.exec 方法的返回值已经提供了对于匹配的捕获组 (Capture Group) 文本与对应的捕获组在正则表达式中的索引。但是,有些场景下我们不仅仅只是希望匹配文本,更需要获得被匹配的文本在输出文本中的起始位置与结束位置,比如我们常用的 VS Code 等开发环境提供语法高亮就需要这些信息。因此,RegExp Match Indices ('d' Flag) 提案期望向 RegExp.prototype.exec 返回的数组对象上,新增 indices 属性用来描述这些位置信息。

const text = "Let's match one:1.";
const regexp = /match\s(?<word>\w+):(?<digit>\d)/gd;

for (const match of text.matchAll(regexp)) {
    console.log(match);
}

上面的代码会输出如下内容:

[
  'match one:1',
  'one',
  '1',
  index: 6,
  input: "Let's match one:1.",
  groups: { word: 'one', digit: '1' },
  indices: {
    0: [6,17],
    1: [12,15],
    2: [16,17],
    groups: { 
      digit: [16, 17],
      word: [12, 15]
    }
  }
]

Top-Level Await

提案链接:https://github.com/tc39/proposal-top-level-await

ECMAScript 2017 开始引入了 Async functions(异步函数)和 await 关键字,此特性大大简化了对 Promise 的使用。不过await只能在 Async functions 内部使用。

新的提案 Top-Level await则允许在 Async functions 之外使用await(例如 CLI 脚本,以及动态导入和数据加载)。该提案将 ES Modules 当做大型的 Async functions,因此这些 ES Modules 可以等待资源加载,这样其他导入这些模块的模块在开始执行自己的代码之前也要等待资源加载完再去执行。

// load-attribute.mjs 
// with top-level await
const data = await (await fetch("https://some.url")).text();
export const attribute = JSON.parse(data).someAttribute;
// main.mjs 
// loaded after load-attribute.mjs is fully loaded
// and its exports are available
import { attribute } from "./load-attribute.mjs";
console.log(attribute);

参考:https://p42.ai/blog/2Alibaba F2E 公众号

展开阅读全文
6 收藏
分享
加载中
最新评论 (8)
不知JavaScript将来有没有可能支持interface(接口)…
2022-01-14 21:00
0
回复
举报
这是经过了多少个版本的迭代哟,再牛B的特性,最后还是要依赖编译器输出成es5的。
2022-01-14 14:07
0
回复
举报
do表达式、|>运算符、::运算符怎么还没进入stage4
2022-01-14 11:16
0
回复
举报
Pokemon哈哈哈
2022-01-14 09:18
0
回复
举报
面向对象编程的 ECMAScript
2022-01-14 09:16
0
回复
举报
js已经浪费太多时间才class语法上了。
还是等模式匹配和管道符吧。
2022-01-14 09:14
1
回复
举报
可能得 等5年
2022-01-14 13:19
0
回复
举报
才知道还有.at这种写法,学到了😁
2022-01-14 09:00
0
回复
举报
更多评论
8 评论
6 收藏
分享
返回顶部
顶部