とほほのES2022入門
ES2022とは
JavaScript(ECMAScript) の2022年版(第13版)です。ES2022 として2022年6月22日に正式承認されました。ES2021 に対して幾つかの新機能が追加されていますが、準備も整っており、すべての機能が Chrome, Firefox, Safari, Node.js などで利用可能です。
ES2022の新機能
トップレベル await
await は async 宣言した関数内からしか呼び出すことができませんでした。トップレベルから呼び出そうとすると、下記の様に無理矢理 async 関数の中に入れる必要がありました。
(async function() {
  await Promise.resolve(console.log('Hello!'));
}());
ES2022 以降、トップレベルでは async 無しでも await を呼び出せるようになりました。関数内から呼び出す場合はこれまで同様 async が必要です。
await Promise.resolve(console.log('Hello!'));
モジュール タイプのスクリプトでのみ使用可能です。通常スクリプトで使用しようとすると下記のエラーとなります。
Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
クラスフィールド宣言
クラスフィールドはコンストラクタの中で宣言する必要がありました。
class Counter {
  constructor() {
    this.count = 0;
  }
  countUp() { this.count++; }
}
var counter = new Counter();
counter.countUp();
console.log(counter.count);
ES2022以降、下記の様に宣言できるようになりました。
class Counter {
  count = 0;
  countUp() { this.count++; }
}
var counter = new Counter();
counter.countUp();
console.log(counter.count);
プライベートフィールド・プライベートメソッド
クラスフィールドはクラス外部からアクセスできてしまうパブリックなものしかなく、_ をつけることで「これはプライベートだから外部からアクセスしちゃダメだよ」という暗黙の作法で運用していました。
class Counter {
  constructor() { this._count = 0 }
  countUp() { this._count++; }
  count() { return this._count; }
}
var counter = new Counter();
counter.countUp();
console.log(counter.count());
console.log(counter._count);		// アクセスできてしまう
ES2022以降、# をつけることでプライベートフィールドを明確に宣言することができるようになりました。
class Counter {
  #count = 0;
  countUp() { this.#count++; }
  count() { return this.#count; }
}
var counter = new Counter();
counter.countUp();
console.log(counter.count());
console.log(counter.#count);		// SyntaxErrorになる
メソッドも # をつけることでプライベートメソッドとして定義することができます。
class Counter {
  #count = 0;
  countUp() { this.#count++; }
  #count() { return this.#count; }
}
static イニシャライズブロック
スタティックフィールドの初期化でなんらかの式やステートメントを使用したい場合、クラス定義の外に記述する必要がありました。
class Foo {
  static x = 123.4;
  static y;
}
Foo.y = Foo.x * 2;
static イニシャライズブロックを利用することで、クラス内部に記述することが可能となりました。
class Foo {
  static x = 123.4;
  static y;
  static {
    Foo.y = Foo.x * 2;
  }
}
プライベートフィールドに対する in 演算子
あるオブジェクトがあるクラスのインスタンスであるかを調べるには instanceof を用います。
class Foo { }
foo = new Foo();
console.log(foo instanceof Foo);
ただし、setPrototypeOf() を使用された場合は正確に判断ができないという問題があります。下記の例で baa は Foo のインスタンスではないのに true となってしまいます。
class Foo { }
class Baa { }
foo = new Foo();
baa = new Baa();
Object.setPrototypeOf(baa, foo);
console.log(baa instanceof Foo);	// trueになってしまう
この問題を解決するためにブランドチェックというテクニックが考案されました。プライベートフィールドである #brand にアクセスできるのは Foo のインスタンスのみであることを利用してチェックメソッドを実装するものです。
class Foo {
  #brand;
  static isFoo(obj) {
    try {
      obj.#brand;
      return true;
    } catch {
      return false;
    }
  }
}
class Baa { }
foo = new Foo();
baa = new Baa();
Object.setPrototypeOf(baa, foo);
console.log(Foo.isFoo(baa));        // false
ただこれだと記述が長くて面倒なので簡単に記述できるようにしたのがプライベートフィールドに対する in 演算子です。
class Foo {
  #brand;
  static isFoo(obj) {
    return #brand in obj;
  }
}
class Baa { }
foo = new Foo();
baa = new Baa();
Object.setPrototypeOf(baa, foo);
console.log(Foo.isFoo(baa));        // false
正規表現の d フラグによる開始・終了インデックス
正規表現に、マッチした部分文字列の開始・終了インデックスを得るための d フラグが追加されました。
var result = "My name is Yamada".match(/My name is (.*)/d); console.log(result.indices[0]); // [0, 17] 文字列全体の開始・終了インデックス console.log(result.indices[1]); // [11, 17] 1個目のマッチ文字列の開始・終了インデックス
?<groupName> による 名前付きキャプチャグループ と組み合わせて使用することもできます。
var result = "My name is Yamada".match(/My name is (?<name>.*)/d);
console.log(result.indices.groups.name);  // [11, 17] nameに対応するマッチ文字列の開始・終了インデックス
Error.cause によるエラーチェイン
funcX() から funcA() や funcB() を呼び出す際、funcX() で下記のような try catch を書くと、funcA() と funcB() のどちらで例外が発生したのかを伝えることができませんでした。
function funcA() { return true; }
function funcB() { throw new Error("FuncB() is failed."); }
function funcX() {
  try {
    funcA();
    funcB();
  } catch (e) {
    throw new Error("FuncX() is failed.");
  }
}
try {
  funcX();
} catch (e) {
  console.log(e);        // FuncX() is failed.
}
ES2022 では Error.cause がサポートされ、funcX() の中で発生した例外情報を cause パラメータで呼び出し元に伝えられるようになりました。
function funcA() { return true; }
function funcB() { throw new Error("FuncB() is failed."); }
function funcX() {
  try {
    funcA();
    funcB();
  } catch (e) {
    throw new Error("FuncX() is failed.", { cause: e });
  }
}
try {
  funcX();
} catch (e) {
  console.log(e);        // FuncX() is failed.
  console.log(e.cause);  // funcB() is failed.
}
下記の様に多重にエラー情報を返却することもできます。
function funcA() { foo; }
function funcB() { try { funcA(); } catch (e) { throw new Error("FuncB() is failed.", { cause: e }); }}
function funcC() { try { funcB(); } catch (e) { throw new Error("FuncC() is failed.", { cause: e }); }}
try {
  funcC();
} catch (e) {
  console.log(e);
  while (e = e.cause) {       // FuncC() is failed.
    console.log(e);           // FuncB() is failed.
  }                           // foo is not defined
}
at(-n)で最後からN番目の要素を取得
String, Array, TypedArray の at() メソッドに負数を指定することで、最後からN番目の要素を簡単に取り出せるようになりました。
const foo = ["Red", "Green", "Blue"];
console.log(foo[foo.length - 1]);	// Blue
const foo = ["Red", "Green", "Blue"];
console.log(foo.at(-1));		// Blue
hasOwn() によるプロパティ保持チェック
オブジェクトが特定のプロパティを保持しているかを判断するのに Object.prototype.hasOwnProperty.call(obj, prop) の代わりに Object.hasOwn(obj, prop) が使用できるようになりました。と言っても、難しいので順を追って説明します。オブジェクトが指定したプロパティを持っているかを調べるには プロパティ名 in オブジェクト を用います。
const obj = { prop1: 1 };
for (const prop in obj) {
  if (prop in obj) {
    console.log(prop);			// prop1
  }
}
しかし、何かのライブラリが Object.prototype を汚染するコードを記載していた場合はそのプロパティまで表示されてしまいます。
Object.prototype.prop2 = 1; // プロトタイプ汚染 const obj = { prop1: 1 }; for (const prop in obj) { if (prop in obj) { console.log(prop); // prop2も表示されてしまう } }
この汚染を避けるために hasOwnProperty() を用います。
Object.prototype.prop2 = 1;
const obj = { prop1: 1 };
for (const prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    console.log(prop);			// prop2は表示されなくなる
  }
}
しかし、オブジェクトが hasOwnProperty() というメソッドを持っていた場合、うまく評価できないという問題が残ります。
Object.prototype.prop2 = 1;
const obj = { prop1: 1, hasOwnProperty: () => true };
for (const prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    console.log(prop);			// prop2も表示されてしまう
  }
}
この問題を避けるために hasOwnProperty() を使用する際は下記の様に呼び出すことが推奨されてきました。ESLint でもそのような警告が出ます。
Object.prototype.prop2 = 1;
const obj = { prop1: 1, hasOwnProperty: () => true };
for (const prop in obj) {
  if (Object.prototype.hasOwnProperty.call(obj, prop)) {
    console.log(prop);			// prop2は表示されなくなる
  }
}
この面倒な記述のショートハンドとして ES2022 で定義されたのが Object.hasOwn() です。
Object.prototype.prop2 = 1;
const obj = { prop1: 1, hasOwnProperty: () => true };
for (const prop in obj) {
  if (Object.hasOwn(obj, prop)) {
    console.log(prop);
  }
}