In 2021, a number of proposals entered Stage 4 of TC39. In the TC39 process, each proposal starts at Stage 0, and moving to Stage 4 means that the proposal has been signed off by the ECMAScript editors and has become a de facto standard feature.

This article lists the 10 proposals that will enter Stage 4 in 2021 and will be incorporated into ECMAScript 2022.

Class Fields

Declares the fields of a class.

Proposal link: https://github.com/tc39/proposal-class-fields

Until now, in the ES specification, class fields were defined and initialised in the class constructor. However, in the new proposal, class fields can be defined and initialised at the top level of the class.

1
2
3
4
5
class Post {
 title;
 content;
 shares = 0;
}

The proposal adds the features described in the following table to the ECMAScript Class (existing features in green).

esfeatures

The features included in the proposal are currently available in Chrome 74, Node 12, Safari Technology Preview 117, TypeScript 3.8, Babel 7.0+ and many more environments. However, it should be noted that because TypeScript, for example, had its own Class field implementation before the proposal officially entered Stage 4, the specific details of the semantics will differ from the preceding ECMAScript standard.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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

This proposal is part of the Class Fields family of proposals, which use the # prefix to define private methods and fields of a class.

Proposal link: https://github.com/tc39/proposal-private-methods

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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

Proposal link: https://github.com/tc39/proposal-static-class-features

Building on the previous class fields and private methods proposal, this proposal adds the features of Static public fields', Static private methods’ and `Static private fields’ to JavaScript classes. The

Public Static Class Fields

Proposal link: https://github.com/tc39/proposal-static-class-features

Building on the previous proposals for fields and private methods for classes, this proposal adds Static public fields, Static private methods and Static private fields features to JavaScript classes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// without static class fields:
class Customer {
  // ...
}
Customer.idCounter = 1;

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

Private Fields In Operator

Detects if private fields exist.

Proposal link: https://github.com/tc39/proposal-private-fields-in-in

Since attempting to access a private field that does not exist on an object raises an exception, there is a need to be able to check whether an object has a given private field.

This proposal provides the use of the in operator to determine whether the field introduced in the Class Private Fields proposal, which was formally introduced in Stage 4 recently, exists in an object. As opposed to directly accessing a private field by try { obj.#foo } catch { /* #foo not existing in obj */ } to determine whether an object has a corresponding field installed, Private-In can distinguish between an access error and the absence of a real field, which would not be distinguished by try-catch in the following scenarios whether it is an access exception or whether the field really does not exist.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class C {
  get #getter() { throw new Error('gotcha'); }
  
  static isC(obj) {
    try { 
      obj.#getter;
      return true;
    } catch {
      return false;
    }
  }
}

And the semantics of the in operator, which is similar to that of a normal field, determines whether a #field exists on an object.

 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;
  }
}

Class Static Initialization Blocks

Proposal link: https://github.com/tc39/proposal-class-static-block

Class Static Initialization Blocks provide a way of evaluating static initialization blocks during class declaration/definition, giving access to Class Private Fields.

Since the advent of Class Private Fields, there has been a constant flow of new practices and needs for class syntax. A similar capability for static initialisation blocks exists in languages such as Java, Static Initialization Blocks.

The initialization blocks defined in the proposal can acquire scope within the class, just like the methods of the class, and imply access to the # fields of the class. With this definition, we can implement the Friend class in JavaScript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

New .at() method on all built-in indexable data.

Proposal link: https://github.com/tc39/proposal-relative-indexing-method

This proposal provides a way to fetch elements from the beginning (forward indexing) or end (reverse indexing) of a string (or array) without using temporary variables.

In many cases, negative indexing of arrays like in Python is very useful. For example, in Python we can access the last element of an array via arr[-1] instead of the current JavaScript method of arr[arr.length-1]. Here the negative number is used as a reverse index from the starting element (i.e. arr[0]).

But the problem in JavaScript now is that the syntax [] is not just used in arrays (and certainly not in Python), and it is not just used as an index in arrays. Referring to a value by index, as in arr[1], is in fact a reference to the "1" property of the object. So arr[-1] is already perfectly usable in today’s JavaScript engines, except that it may not mean what we want it to mean: it refers to the "-1" property of the target object, rather than being an inverted index.

This proposal provides a generic solution, allowing access to any reverse-indexed, or forward-indexed, element via the .at method on any indexable type (Array, String, and TypedArray).

1
2
3
4
5
6
7
// 数组
[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

Proposal link: [https://github.com/tc39/proposal-accessible-object-hasownproperty](https://github.com/tc39/proposal-accessible-object- hasownproperty)

In short, the proposal is to use Object.hasOwn instead of Object.prototype.hasOwnProperty.call, a cleaner and more reliable way to check if a property is set directly on an object.

Error Cause

Proposal link: https://github.com/tc39/proposal-error-cause

Error Cause is a proposal by Alibaba and is claimed to be the first Chinese TC39 proposal to make it to Stage 4.

This proposal adds a new attribute cause to the Error constructor in JavaScript, which allows developers to attach an error cause to a thrown error to clearly pass error context information across multiple call stacks. Specifically, the proposal adds an optional parameter options to the Error Constructor, which sets cause and accepts any JavaScript value (JavaScript can throw any value, such as undefined or a string), which is assigned to the newly created error.cause.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)

Proposal link: https://github.com/tc39/proposal-regexp-match-indices

The return value of the RegExp.prototype.exec method in the current ECMAScript already provides the index in the regular expression for the text of the matching Capture Group and the corresponding Capture Group. However, in some cases we don’t just want to match text, we need to get the start and end position of the text being matched in the output text, for example, we need this information to provide syntax highlighting in development environments such as VS Code. Therefore, the RegExp Match Indices (’d’ Flag) proposal expects to add the indices property to describe this positional information.

1
2
3
4
5
6
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);
}

The above code would output the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[
  '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

Proposal link: https://github.com/tc39/proposal-top-level-await

ECMAScript 2017 introduces Async functions and the await keyword, a feature that greatly simplifies the use of Promise. However, await can only be used inside Async functions.

The new proposal Top-Level await allows await to be used outside of Async functions (e.g. CLI scripts, as well as dynamic import and data loading). This proposal treats ES Modules as large Async functions, so that these ES Modules can wait for resources to be loaded, so that other modules importing these modules will have to wait for resources to be loaded before they can start executing their own code.

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