ECMAScript 2022 正式釋出,有哪些新特性?

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第 11 天,點選檢視活動詳情


大家好,我是 CUGGZ。

2022 年 6 月 22 日,第 123 屆 ECMA 大會批准了 ECMAScript 2022 語言規範,這意味著它現在正式成為標準。下面就來看看 ECMAScript 2022 新增了哪些特性!

image.png

總覽:

  1. Top-level Await
  2. Object.hasOwn()
  3. at()
  4. error.cause
  5. 正則表示式匹配索引
  6. 類的例項成員

1. Top-level Await

在ES2017中,引入了 async 函式和 await 關鍵字,以簡化 Promise 的使用,但是 await 關鍵字只能在 async 函式內部使用。嘗試在非同步函式之外使用 await 就會報錯:SyntaxError - SyntaxError: await is only valid in async function

頂層 await 允許我們在 async 函式外面使用 await 關鍵字。它允許模組充當大型非同步函式,通過頂層 await,這些 ECMAScript 模組可以等待資源載入。這樣其他匯入這些模組的模組在執行程式碼之前要等待資源載入完再去執行。

由於 await 僅在 async 函式中可用,因此模組可以通過將程式碼包裝在 async 函式中來在程式碼中包含 await: ```javascript // a.js import fetch from "node-fetch"; let users;

export const fetchUsers = async () => { const resp = await fetch('https://jsonplaceholder.typicode.com/users'); users = resp.json(); } fetchUsers();

export { users };

// usingAwait.js import {users} from './a.js'; console.log('users: ', users); console.log('usingAwait module');

我們還可以立即呼叫頂層`async`函式(IIAFE):javascript import fetch from "node-fetch"; (async () => { const resp = await fetch('https://jsonplaceholder.typicode.com/users'); users = resp.json(); })(); export { users }; 這樣會有一個缺點,直接匯入的 `users` 是 `undefined`,需要在非同步執行完成之後才能訪問它:javascript // usingAwait.js import {users} from './a.js';

console.log('users:', users); // undefined

setTimeout(() => { console.log('users:', users); }, 100);

console.log('usingAwait module'); `` 當然,這種方法並不安全,因為如果非同步函式執行花費的時間超過100毫秒, 它就不會起作用了,users仍然是undefined`。

另一個方法是匯出一個 promise,讓匯入模組知道資料已經準備好了: ```javascript //a.js import fetch from "node-fetch"; export default (async () => { const resp = await fetch('https://jsonplaceholder.typicode.com/users'); users = resp.json(); })(); export { users };

//usingAwait.js import promise, {users} from './a.js'; promise.then(() => { console.log('usingAwait module'); setTimeout(() => console.log('users:', users), 100); }); ``` 雖然這種方法似乎是給出了預期的結果,但是有一定的侷限性:匯入模組必須瞭解這種模式才能正確使用它

而頂層await就可以解決這些問題: ```javascript // a.js const resp = await fetch('https://jsonplaceholder.typicode.com/users'); const users = resp.json(); export { users};

// usingAwait.js import {users} from './a.mjs';

console.log(users); console.log('usingAwait module'); `` 頂級await` 在以下場景中將非常有用:

  • 動態載入模組: javascript const strings = await import(`/i18n/${navigator.language}`);

  • 資源初始化: javascript const connection = await dbConnector();

  • 依賴回退: javascript let translations; try { translations = await import('https://app.fr.json'); } catch { translations = await import('https://fallback.en.json'); } 該特性的瀏覽器支援如下:

2. Object.hasOwn()

在ES2022之前,可以使用 Object.prototype.hasOwnProperty() 來檢查一個屬性是否屬於物件。

Object.hasOwn 特性是一種更簡潔、更可靠的檢查屬性是否直接設定在物件上的方法: ```javascript const example = { property: '123' };

console.log(Object.prototype.hasOwnProperty.call(example, 'property')); console.log(Object.hasOwn(example, 'property')); ``` 該特性的瀏覽器支援如下:

3. at()

at() 是一個數組方法,用於通過給定索引來獲取陣列元素。當給定索引為正時,這種新方法與使用括號表示法訪問具有相同的行為。當給出負整數索引時,就會從陣列的最後一項開始檢索: ```javascript const array = [0,1,2,3,4,5];

console.log(array[array.length-1]); // 5 console.log(array.at(-1)); // 5

console.log(array[array.lenght-2]); // 4 console.log(array.at(-2)); // 4 除了陣列,字串也可以使用`at()`方法進行索引:javascript const str = "hello world";

console.log(str[str.length - 1]); // d console.log(str.at(-1)); // d ```

4. error.cause

在 ECMAScript 2022 規範中,new Error() 中可以指定導致它的原因: javascript function readFiles(filePaths) { return filePaths.map( (filePath) => { try { // ··· } catch (error) { throw new Error( `While processing ${filePath}`, {cause: error} ); } }); }

5. 正則表示式匹配索引

該特性允許我們利用 d 字元來表示我們想要匹配字串的開始和結束索引。以前,只能在字串匹配操作期間獲得一個包含提取的字串和索引資訊的陣列。在某些情況下,這是不夠的。因此,在這個規範中,如果設定標誌 /d,將額外獲得一個帶有開始和結束索引的陣列。 ```javascript const matchObj = /(a+)(b+)/d.exec('aaaabb');

console.log(matchObj[1]) // 'aaaa' console.log(matchObj[2]) // 'bb' 由於 `/d` 標識的存在,`matchObj`還有一個屬性`.indices`,它用來記錄捕獲的每個編號組:javascript console.log(matchObj.indices[1]) // [0, 4] console.log(matchObj.indices[2]) // [4, 6] 我們還可以使用命名組:javascript const matchObj = /(?a+)(?b+)/d.exec('aaaabb');

console.log(matchObj.groups.as); // 'aaaa' console.log(matchObj.groups.bs); // 'bb' `` 這裡給兩個字元匹配分別命名為asbs,然後就可以通過groups`來獲取到這兩個命名分別匹配到的字串。

它們的索引儲存在 matchObj.indices.groups 中: javascript console.log(matchObj.indices.groups.as); // [0, 4] console.log(matchObj.indices.groups.bs); // [4, 6] 匹配索引的一個重要用途就是指向語法錯誤所在位置的解析器。下面的程式碼解決了一個相關問題:它指向引用內容的開始和結束位置。 ```javascript const reQuoted = /“([^”]+)”/dgu; function pointToQuotedText(str) { const startIndices = new Set(); const endIndices = new Set(); for (const match of str.matchAll(reQuoted)) { const [start, end] = match.indices[1]; startIndices.add(start); endIndices.add(end); } let result = ''; for (let index=0; index < str.length; index++) { if (startIndices.has(index)) { result += '['; } else if (endIndices.has(index+1)) { result += ']'; } else { result += ' '; } } return result; }

console.log(pointToQuotedText('They said “hello” and “goodbye”.')); // ' [ ] [ ] ' ```

6. 類的例項成員

(1)公共例項欄位

公共類欄位允許我們使用賦值運算子 (=) 將例項屬性新增到類定義中。下面是一個計數器的例子: ```javascript import React, { Component } from "react";

export class Incrementor extends Component { constructor() { super(); this.state = { count: 0, }; this.increment = this.increment.bind(this); }

increment() { this.setState({ count: this.state.count + 1 }); }

render() { return ( ); } } 在這個例子中,在建構函式中定義了例項欄位和繫結方法,通過新的類語法,可以使程式碼更加直觀。新的公共類欄位語法允許我們直接將例項屬性作為屬性新增到類上,而無需使用建構函式方法。這樣就簡化了類的定義,使程式碼更加簡潔、可讀:javascript import React from "react";

export class Incrementor extends React.Component { state = { count: 0 };

increment = () => this.setState({ count: this.state.count + 1 });

render = () => ( ); } `` 有些小夥伴可能就疑問了,這個功能很早就可以使用了呀。但是它現在還不是標準的 ECMAScript,預設是不開啟的,如果使用create-react-app建立 React 專案,那麼它預設是啟用的,否則我們必須使用正確的babel外掛才能正常使用(@babel/preset-env`)。

下面來看看關於公共例項欄位的注意事項:

  • 公共例項欄位存在於每個建立的類例項上。它們要麼是在Object.defineProperty()中新增,要麼是在基類中的構造時新增(建構函式主體執行之前執行),要麼在子類的super()返回之後新增: ```javascript class Incrementor { count = 0 }

const instance = new Incrementor(); console.log(instance.count); // 0 ```

  • 未初始化的欄位會自動設定為 undefined: ```javascript class Incrementor { count }

const instance = new Incrementor(); console.assert(instance.hasOwnProperty('count')); console.log(instance.count); // undefined ```

  • 可以進行欄位的計算: ```javascript const PREFIX = 'main';

class Incrementor { [${PREFIX}Count] = 0 }

const instance = new Incrementor(); console.log(instance.mainCount); // 0 ```

(2)私有例項欄位、方法和訪問器

預設情況下,ES6 中所有屬性都是公共的,可以在類外檢查或修改。下面來看一個例子: ```javascript class TimeTracker { name = 'zhangsan'; project = 'blog'; hours = 0;

set addHours(hour) { this.hours += hour; }

get timeSheet() { return ${this.name} works ${this.hours || 'nothing'} hours on ${this.project}; } }

let person = new TimeTracker(); person.addHours = 2; // 標準 setter person.hours = 4; // 繞過 setter 進行設定 person.timeSheet; `` 可以看到,在類中沒有任何措施可以防止在不呼叫setter` 的情況下更改屬性。

而私有類欄位將使用雜湊#字首定義,從上面的示例中,可以修改它以包含私有類欄位,以防止在類方法之外更改屬性: ```javascript class TimeTracker { name = 'zhangsan'; project = 'blog'; #hours = 0; // 私有類欄位

set addHours(hour) { this.#hours += hour; }

get timeSheet() { return ${this.name} works ${this.#hours || 'nothing'} hours on ${this.project}; } }

let person = new TimeTracker(); person.addHours = 4; // 標準 setter person.timeSheet // zhangsan works 4 hours on blog 當嘗試在 `setter` 方法之外修改私有類欄位時,就會報錯:javascript person.hours = 4 // Error Private field '#hours' must be declared in an enclosing class 還可以將方法或 `getter/setter` 設為私有,只需要給這些方法名稱前面加`#`即可:javascript class TimeTracker { name = 'zhangsan'; project = 'blog'; #hours = 0; // 私有類欄位

set #addHours(hour) { this.#hours += hour; }

get #timeSheet() { return ${this.name} works ${this.#hours || 'nothing'} hours on ${this.project}; }

constructor(hours) { this.#addHours = hours; console.log(this.#timeSheet); } }

let person = new TimeTracker(4); // zhangsan works 4 hours on blog 由於嘗試訪問物件上不存在的私有欄位會發生異常,因此需要能夠檢查物件是否具有給定的私有欄位。可以使用 `in` 運算子來檢查物件上是否有私有欄位:javascript class Example { #field

static isExampleInstance(object) { return #field in object; } } ```

(3)靜態公共欄位

在ES6中,不能在類的每個例項中訪問靜態欄位或方法,只能在原型中訪問。ES 2022 提供了一種在 JavaScript 中使用 static 關鍵字宣告靜態類欄位的方法。下面來看一個例子: ```javascript class Shape { static color = 'blue';

static getColor() { return this.color; }

getMessage() { return color:${this.color} ; } } 可以從類本身訪問靜態欄位和方法:javascript console.log(Shape.color); // blue console.log(Shape.getColor()); // blue console.log('color' in Shape); // true console.log('getColor' in Shape); // true console.log('getMessage' in Shape); // false 例項不能訪問靜態欄位和方法:javascript const shapeInstance = new Shape(); console.log(shapeInstance.color); // undefined console.log(shapeInstance.getColor); // undefined console.log(shapeInstance.getMessage());// color:undefined 靜態欄位只能通過靜態方法訪問:javascript console.log(Shape.getColor()); // blue console.log(Shape.getMessage()); //TypeError: Shape.getMessage is not a function 這裡的 `Shape.getMessage()` 就報錯了,因為 `getMessage` 不是一個靜態函式,所以它不能通過類名 `Shape` 訪問。可以通過以下方式來解決這個問題:javascript getMessage() { return color:${Shape.color} ; } 靜態欄位和方法是從父類繼承的:javascript class Rectangle extends Shape { }

console.log(Rectangle.color); // blue console.log(Rectangle.getColor()); // blue console.log('color' in Rectangle); // true console.log('getColor' in Rectangle); // true console.log('getMessage' in Rectangle); // false ```

(4)靜態私有欄位和方法

與私有例項欄位和方法一樣,靜態私有欄位和方法也使用雜湊 (#) 字首來定義: ```javascript class Shape { static #color = 'blue';

static #getColor() { return this.#color; }

getMessage() { return color:${Shape.#getColor()} ; } } const shapeInstance = new Shape(); shapeInstance.getMessage(); // color:blue 私有靜態欄位有一個限制:只有定義私有靜態欄位的類才能訪問該欄位。這可能在使用 `this` 時導致出乎意料的情況:javascript class Shape { static #color = 'blue'; static #getColor() { return this.#color; } static getMessage() { return color:${this.#color} ; } getMessageNonStatic() { return color:${this.#getColor()} ; } }

class Rectangle extends Shape {}

console.log(Rectangle.getMessage()); // Uncaught TypeError: Cannot read private member #color from an object whose class did not declare it const rectangle = new Rectangle(); console.log(rectangle.getMessageNonStatic()); // TypeError: Cannot read private member #getColor from an object whose class did not declare it 在這個例子中,`this` 指向的是 `Rectangle` 類,它無權訪問私有欄位 `#color`。當我們嘗試呼叫 `Rectangle.getMessage()` 時,它無法讀取 `#color` 並丟擲了 `TypeError`。可以這樣來進行修改:javascript class Shape { static #color = 'blue'; static #getColor() { return this.#color; } static getMessage() { return ${Shape.#color}; } getMessageNonStatic() { return color:${Shape.#getColor()} color; } }

class Rectangle extends Shape {} console.log(Rectangle.getMessage()); // color:blue const rectangle = new Rectangle(); console.log(rectangle.getMessageNonStatic()); // color:blue ```

(5)類靜態初始化塊

靜態私有和公共欄位只能讓我們在類定義期間執行靜態成員的每個欄位初始化。如果我們需要在初始化期間像 try…catch 一樣進行異常處理,就不得不在類之外編寫此邏輯。該規範就提供了一種在類宣告/定義期間評估靜態初始化程式碼塊的優雅方法,可以訪問類的私有欄位。

先來看一個例子: ```javascript class Person { static GENDER = "Male" static TOTAL_EMPLOYED; static TOTAL_UNEMPLOYED;

try {
    // ...
} catch {
    // ...
}

} 上面的程式碼就會引發錯誤,可以使用類靜態塊來重構它,只需將`try...catch`包裹在 `static` 中即可:javascript class Person { static GENDER = "Male" static TOTAL_EMPLOYED; static TOTAL_UNEMPLOYED;

static { try { // ... } catch { // ... } } } 此外,類靜態塊提供對詞法範圍的私有欄位和方法的特權訪問。這裡需要在具有例項私有欄位的類和同一範圍內的函式之間共享資訊的情況下很有用。javascript let getData;

class Person { #x

constructor(x) { this.#x = { data: x }; }

static { getData = (obj) => obj.#x; } }

function readPrivateData(obj) { return getData(obj).data; }

const john = new Person([2,4,6,8]);

readPrivateData(john); // [2,4,6,8] `` 這裡,Person類與readPrivateData` 函式共享了私有例項屬性。