ECMAScript 2022 預覽:10 個在 2021 進入 Stage 4 的提案

2022-01-14 09:00:06

2021 年,多項提案進入了 TC39 的 Stage 4 階段。按照 TC39 的運作流程,每個提案都從 Stage 0 開始,而進入 Stage 4 則意味著該提案已被 ECMAScript 編輯簽署同意意見,成為了事實上的標準特性。

本文整理了在 2021 年進入 Stage 4 的 10 個提案,它們將會被吸納進 ECMAScript 2022。


宣告類的欄位。

提案連結:

到目前為止,在 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);

該提案屬於 Class Fields 系列提案的一部分,其使用#字首定義類的私有方法和欄位。

提案連結:

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

提案連結:

在之前的類的欄位和私有方法提案的基礎上,該提案為 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;
  // ...
}

檢測私有欄位是否存在。

提案連結:

由於嘗試存取物件上不存在的私有欄位會引發異常,因此需要能夠檢查物件是否具有給定的私有欄位。

這個提案提供了使用 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

提案連結:

類靜態初始化塊 (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();

在所有內建的可索引資料上新增 .at() 方法。

提案連結:

該提案提供了一種從字串(或陣列)的開頭(正向索引)或結尾(反向索引)獲取元素的方法,無需使用臨時變數。

很多時候,類似於 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替代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 是阿里巴巴提出的提案,據稱也是中國首個進入 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
}

提案連結:

當前 ECMAScript 中的 RegExp.prototype.exec 方法的返回值已經提供了對於匹配的捕獲組 (Capture Group) 文字與對應的捕獲組在正規表示式中的索引。但是,有些場景下我們不僅僅只是希望匹配文字,更需要獲得被匹配的文字在輸出文字中的起始位置與結束位置,比如我們常用的 VS Code 等開發環境提供語法高亮就需要這些資訊。因此, 提案期望向 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]
    }
  }
]

提案連結:

ECMAScript 2017 開始引入了 Async functions(非同步函數)和 await 關鍵字,此特性大大簡化了對 Promise 的使用。不過await只能在 Async functions 內部使用。

新的提案 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);

參考:、

展開閱讀全文