ES2022 學習筆記

隨著時間推進,ES 規範也在不斷推陳出新,本篇紀錄 ES2022 的幾個值得使用的新語法~(有些其實去年就已經可以使用了!!)

Introduce

Top-level await

以往在全局環境並不支援使用 await,ES2022 後在瀏覽器端使用 esmodule 時,將可在 top level 使用 await 語法,需要注意使用 top level await 會讓依賴的 module 等待其執行完畢,使用時要特別小心

下面範例我們在 esmodule 中的 index.js 引入 a.js,並在 import 行為的前後 print

// a.js
const a = () => new Promise((res) => setTimeout(res, 4000));

await a(); // top level await

export default 'done';
// index.js
console.log('start loading moduleA', moduleA);
import moduleA from './a.js';
console.log('moduleA imported', moduleA);

最終執行的順序會如下:

// wait 4 seconds
// start loading moduleA done
// moduleA imported done

儘管我們在 import 前使用 console 並引用 moduleA 變數,實際執行時,import 動作會優先處理依賴,若引入的模組具有 top level await 時,也導致 index.js 發生執行延遲,這是使用上必須要特別注意的!!

實測在最新 chrome 已經可以使用,可以看到若不是在 esmodule 使用 top level await 會噴錯: Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules

Array.at()

一直以來在 js 中若要取得最後一個元素我們會使用 arr[arr.length - 1] 這種方式,既不直覺,又不香,常常看著隔壁 python 垂涎,但因為 js 的 [] 語法也同時用在 object keys 檢索上,貼心的團隊便給了一個 at() 方法讓我們也可以香噴噴的拿到最後一個元素了,實測最新 chrome 也已支援

const arr = [1, 2];
console.log(arr.at(0)); // 2
console.log(arr.at(-1)); // 2

Object.hasOwn

Object.prototype.hasOwnProperty 一直以來被人詬病名子太長不好使用,新的語法將可直接從 Object 中使用 hasOwn 調用

const obj = { a: 1 };
console.log(Object.hasOwn(obj, 'a'));

Error Cause

錯誤處理非常重要,但有時我們的客製化錯誤處理會導致資訊傳遞失真而難以排查真實錯誤訊息,常見錯誤處理方式如下

// x 不存在
const a = () => console.log(x);
const b = () => a();
const c = () => b();

const myJob = () => {
  try {
    c();
  } catch(err) {
    // throw err; // 真實錯誤
    throw Error('something wrong!'); // 拋出客製化錯誤
  }
}

try {
  myJob();
} catch (err) {
  console.log(err);
}

我們會在函數中包裹 try catch 處理未知錯誤,並返回客製化的訊息,但這麼做卻會導致原本的錯誤層級失真如下

// 真實的錯誤
ReferenceError: x is not defined
  at a (xxxx)
  at b (xxxx)
  at c (xxxx)
  at myJob (xxxx)
  at xxxx

// 客製化錯誤
Error: something wrong!
  at myJob (xxxx)
  at xxxx

實際發生錯誤的位置是在 a 方法中,但我們的客製化錯誤導致在他之前的訊息被截斷,從而可能導致排查錯誤困難,也因此我們會實作如下的方式去保留原始錯誤方便除錯

const myJob = () => {
  try {
    c();
  } catch(err) {
    const myError = new Error('something wrong!');
    myError.cause = err; // 保留真實錯誤
    throw myError;
  }
}

try {
  myJob();
} catch (err) {
  console.log(err); // 取得客製化錯誤
  console.log(err.cause); // 取得真實錯誤
}

但這麼做還是很多餘,總不能每次處理都要多寫這些,新的提案讓我們能方便快速的做這件事,只需要在 ErrorConstructor 中提供 { cause: err } 就行,方便又美觀~

const myJob = () => {
  try {
    c();
  } catch(err) {
    throw Error('something wrong!', { cause: err });
  }
}

Class Private Declarations

雖然在 Typescript 中已經有 private 語法讓我們能替類別定義私有屬性、方法,在 pure js 當中卻一直沒有一種原生方式做到這件事,但在新的提案中,我們可以透過使用 # 開頭標記我們的私有屬性、方法

class MyClass {
  #name = 'MyClass';

  constructor() {
    this.#init();
  }

  #init() {
    console.log('init in private');
  }

  // 拿來做類型檢查也很方便
  static check(obj) {
    return #name in obj && obj.#name === 'MyClass';
  }
}

const myInstance = new MyClass();
console.log(myInstance.#a);
// Uncaught SyntaxError: Private field '#a' must be declared in an enclosing class (at xxxx)
console.log(myInstance.#init());
// Uncaught SyntaxError: Private field '#init' must be declared in an enclosing class (at xxxx)
console.log(MyClass.check(myInstance));
// true
console.log(MyClass.check({}));
// false

Class Static Block

目前的 static 語法僅可用作單一靜態屬性、方法的定義上,若靜態屬性彼此關聯依賴時,必須拉到 class 外部進行定義,如下面這種方式:

// without static blocks:
class C {
  static x = 'X';
  static y;
  static z;
}

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

使用新的提案 static block 靜態區塊的話,我們可以在一個區塊中進行相關操作,這些靜態區塊也同樣只會執行一次

class C {
  static x = '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 = ...;
    }
  }
}

此語法也適用常見的靜態屬性方法獲取私有屬性場景

class MyClass {
  #count = 0;

  static getCount(obj) {
    return obj.#count;
  }

  increment() {
    this.#count += 1;
  }
}

const myClass = new MyClass();
console.log(MyClass.getCount(myClass)); // 0

這個新語法也讓我們能將上面這種需求改寫如下面這樣

let getCount;

class MyClass {
  #count = 0;

  static {
    getCount = (obj) => obj.#count;
  }

  increment() {
    this.#count += 1;
  }
}

const myClass = new MyClass();
console.log(getCount(myClass)); // 0

感謝大家收看,下次再見拉~

Last Updated:
Contributors: johnnywang