JavaScriptの次の仕様ES2022の新機能まとめ

196
70
211

JavaScriptの仕様であるECMAScriptはEcma Internationalによって定められています。ECMAScript 2015(ES6)の登場以降は、ECMAScript 2016、ECMAScript 2017・・・と、年次で仕様が更新されています。最新のECMAScript 2022(ES2022)は今月6月22日のEcma InternationalのGA 123rd meetingにて承認される見込みです。

ES2022はすでに多くのブラウザやNode.js環境で利用可能です。本記事では新仕様と使いどころを紹介します。

Array, String, TypedArray の .at()

配列や文字列の番号でアクセスができるメソッドとして、at()が用意されました。

配列記号[]とインデックスを使うことで配列の要素にアクセスできますが、ES2022ではat()メソッドを使うことでもアクセスできるようになります。

const array = ["a", "b", "c"];

console.log(array[1]); // "b"

// ES2022
console.log(array.at(1)); // "b"

これだけだと利点は少なそうですが、at()メソッドには負の整数も指定できます。負の整数を指定すると配列の末尾から参照できます。従来だと配列のlengthを使って末尾を参照する必要がありましたが、at()メソッドだと直感的に記述できます。-1が配列の末尾を示します。

const array = ["a", "b", "c", "d"];

// 従来の配列末尾からの参照
console.log(array[array.length - 1]); // "d"

// ES2022で可能になる書き方
console.log(array.at(-1)); // "d"

at()メソッドに非数や小数を利用すると複雑な結果が得られます(記事『【追記あり】ES2022 Array#at がちょっとおかしい #fix_ecmascript_at - Qiita』)。整数以外は引数に渡さないように注意しましょう。

配列だけでなく、文字列でもat()メソッドを利用できます。

const myString = "あいうえお";

// 先頭から参照
console.log(myString.at(1)); // "い"
console.log(myString[1]); // "い"

// 末尾から参照
console.log(myString[myString.length - 2]); // "え"
console.log(myString.at(-2)); // "え"

Top-Level Await

awaitasyncキーワードはES2017で登場しました。awaitasyncキーワードはPromiseでの非同期処理が扱いやすくなったことで、現在の多くのフロントエンドエンジニアは利用していることでしょう。

従来のawaitは、asyncとした関数宣言・関数式でしか利用できませんでしたが、ES2022でasyncなしでもトップレベルでawaitが利用できるようになりました。

たとえば、従来のコードとしては次のように記述しました。ラッパーとしての関数(start)を用意しています。

<script>
  // 従来のコード
  const start = async () => {
    // JSONデータを読み込む
    const data = await fetch("./example/data.json");
    const object = await data.json();
  }
  start();
</script>

ES2022ではラッパーとしてのasync関数を宣言せずにawaitを利用できています。

<script type="module">
  // ES2022で可能になった書き方
  const data = await fetch("./example/data.json");
  const object = await data.json();
  console.log(object)
</script>

asyncの関数宣言・関数式を用意する手間がなくなるので、書き捨てのコードをサクッと試すのに役立つでしょう。

  • ブラウザの開発者ツールのConsoleパネルでコードをサクッと試したいとき
  • Node.jsでサクッとコードを試したいとき

注意点として、asyncなしで記述できるのはトップレベルであり、async無しの関数宣言・関数式のなかではawaitは利用できません。機能名のとおり「Top-Level Await」であること認識しましょう。

NG例:

// 🆖 失敗するコード
const start = () => { // async 宣言がないアロー関数式
  // JSONデータを読み込む
  const data = await fetch("./example/data.json"); // ❎️ シンタックスエラー
  const object = await data.json();
}
start();

また、Top-Level AwaitはES Moduleとしてしか利用できません。scriptタグで利用する場合は、<script type="module">とES Module方式で利用しなければなりません。CDNから<script>タグを貼り付ける・・・といった場面では利用できません。ES Moduleの使い方は記事『ブラウザで覚えるES Modules入門』を参照ください。Node.jsの場合は、拡張子を.mjsとしてES Moduleであることを指定します。

このTop-Level Awaitは、本領として動的モジュール読み込みのimport()と組み合わせると効果を発揮します。

// 外部ファイルに宣言したモジュール
const {someModule} = await import("./someModule.mjs");

console.log(someModule);

次のように動的importには文字列を利用できるので、実行環境に応じて文言リストを読み込むといった使い方もできます。モジュール方式のJavaScriptでの利用がしやすくなります。

// ブラウザの実行言語に応じて文言リストを取得するコード
const {wordingList} = await import(`./i18l/${navigaor.language}.js`);

console.log(wordingList);

TypeScript 3.8(2020年2月リリース)から利用できたので、すでに馴染みのある方が多いかもしれません。

プライベートのインスタンスフィールド、メソッド

ECMAScript 2015でクラス構文が登場しました。しかし、他の言語から比べるとES2015のclassは十分な機能を有しているとはいえない状況でした。ES2022では以下の機能を中心に仕様が追加されています。

  • クラスインスタンスのプライベートのフィールド
  • クラスインスタンスのプライベートのメソッド
  • プライベートを示すアクセサー

プライベートフィールド(プライベート変数)には接頭辞に#(ハッシュ)を利用します。プライベートフィールドは外部からアクセスできず、もしアクセスしようとすると実行時エラーが発生します。オブジェクト指向プログラミングの観点としては、クラス内に外部からアクセスできない変数を設けることで、カプセル化を実現するのに役立ちます。

class MyCounter {
  #count;

  constructor(count) {
    this.#count = count;
  }

  #calc() {
    return this.#count * 10;
  }

  say() {
    // プライベート変数にアクセスできるのはクラスの内部だけ
    console.log(this.#calc());
  }
}

const object = new MyCounter(3);
object.say(); // 30

console.log(object.#count); // ❎️ シンタックスエラー
console.log(object.#calc()); // ❎️ シンタックスエラー

従来では、プライベートフィールドを設けることができなかったので、接頭語に「_」(アンダースコア)を使い、命名規則でプライベートフィールドであることを示していたエンジニアも多いと思います。接頭語に「_」を使ったとしても言語的にはプライベートであることを強制できないので、アクセスしようと思えば自由にアクセスできました。ES2022の新しい接頭辞#を使うことで、自由にアクセスできないようになり、プライベートフィールドの安全性が高まります。

関連して、inを使って外部からアクセスしようとしても成功しません。これもES2022の仕様として定義されています。

class MyCounter {
  #count;

  constructor(count) {
    this.#count = 0;
  }
  hasCount () {
    return #count in this;
  }
}

const object = new MyCounter();
console.log(object.hasCount()); // true
console.log("#count" in object); // false

参考文献

TypeScriptのprivateと#の違い

TypeScriptではプライベートのアクセス修飾子としてprivateが存在しました(ソフトプライベート)。TypeScript 3.8以降はES2022と同様の#を利用できます(ハードプライベート)。TypeScriptの設定ファイルtsconfig.jsonのターゲットによってコンパイル結果が異なりますが、おおよそ以下の出力となります。

  • TypeScriptでは、private#へ変換されません。
  • privateアクセス修飾子はターゲットにかかわらず互換コードへコンパイルされます(取り除かれた形になります)。
  • #はES2015〜SES2021ターゲットだと、互換コードへコンパイルされます(ES5以下にはコンパイルできません)。
  • #はES2022ターゲットだと、そのまま出力されます。

private#のどちらを利用するかは開発チームで足並みを揃えたほうがいいでしょう。

静的クラスフィールド

クラスのstaticフィールドが使えるようになりました。クラス内部でstaticを宣言すると、インスタンス化せずともクラスに固有のフィールドとしてアクセスできます。

従来でも、クラスの外部で動的にフィールドを追加すると、静的フィールドのように扱う事ができました。ES2022ではクラス内部にstaticフィールドを宣言できるので、よりクラスらしい書き方ができるようになりました。

// 従来の書き方
class MyOldClass {
  // ...
}

MyOldClass.message = "あ"; // 動的に追加することで静的変数を表現できた

// 新しい書き方
class MyClass {
  static message = "い";
  // ...
}

console.log(MyOldClass.message); // "あ"
console.log(MyClass.message); // "い"

クラスの静的メソッドも定義できます。次の例ではsayHello()というメソッドをstaticフィールドに定義しています。

// 新しい書き方
class MyClass {
  static message = "あ";
  static sayHello() {
    console.log(MyClass.message); // "あ"
  }
}

MyClass.sayHello();

staticフィールドには#接頭辞が利用でき、静的なプライベートフィールドとして定義できます。

次のコードでは、クラスの部的なIDを生成する機能を静的なプライベートフィールドで用意しています。クラスの内部であるコンストラクターからは呼び出せますが、外部から呼び出せないため、意図しない呼び出しを防げます。

class MyCounter {
  static #counter = 1;

  // 静的なプライベート関数
  static #getNextId() {
    return MyCounter.#counter++;
  }

  #id; // プライベート変数としてのID

  constructor() {
    // 内部的に管理したいIDには、静的変数を利用する
    this.#id = MyCounter.#getNextId();
  }

  getId () {
    return this.#id;
  }
}

// 利用例
const list = [new MyCounter(), new MyCounter()]
console.log(list[1].getId()); // 2

クラスの静的イニシャライザーブロック

静的イニシャライザーブロックを使うと、クラスの評価中にコードを実行できます。staticキーワードで宣言したブロックステートメント内にコードを記述します。このブロックステートメント内のコードは、クラスが評価されるときに1度だけ実行されます。「静的変数に値を格納したいが、クラス宣言のタイミングでないと初期値を入れられない」といった場面で利用できます。

class MyClass {
  static #myProperty;

  // 静的イニシャライザーブロック
  static {
    // 外部からデータととってくるとか、環境変数から加工するとか、複雑な処理等
    // 以下はダミーの処理
    const json = JSON.parse(`{"someField": "hoge"}`);
    this.#myProperty = json.someField;
  }
  constructor() {
    console.log(MyClass.#myProperty);
  }
}

new MyClass(); // "hoge"

TypeScriptでは同等機能がTypeScript 4.4(2021年8月リリース)以降で利用できました。

Object.hasOwn()メソッド

オブジェクトにプロパティーが存在するか確認するのが簡単になります。

従来では以下のメソッドを呼び出すことで、オブジェクト内にプロパティーが存在するか確認していました。

const example = {
  property: "あいう",
};

console.log(Object.prototype.hasOwnProperty.call(example, "property"));
console.log(example.hasOwnProperty("property")); // この書き方は動作するが注意が必要

JavaScriptはhasOwnPropertyプロパティ名が保護されていないので(書き換えることができるので)、安全のためにはObject.prototype.hasOwnProperty.call()を使う必要がありました。しかし、Object.prototype.hasOwnProperty.call()と記載するのも長くて手間です。

ES2022ではObject.hasOwn()メソッドで同じ事ができるようになりました。

const example = {
  property: "あいう",
};

console.log(Object.hasOwn(example, "property")); // true

参考文献

Error.cause

Errorオブジェクトにcauseというフィールドが追加されました。これは、エラーの発生源を格納できるようになります。Errorのコンストラクターの第2引数にcauseフィールドを含むオブジェクトを指定することで利用できます。

throw new Error("失敗", { cause: error }); // error は元となるエラーオブジェクト

従来の課題

従来だとcauseプロパティーがなかったため、try catchでラッパーを囲っていくと、元になるエラー起因を追跡するのが困難でした。

以下にネットワークエラーによる例を示します。fetchメソッドを使ってウェブサーバーからJSONファイルを受信するコードをサンプルとして用意しました。fetchでJSONファイルを読み込むときは以下の3点のエラーのケアが必要でしょう。

  1. オフライン時のネットワークエラー
  2. サーバーレスポンス404のエラー
  3. JSONのパースエラー

ラッパーの関数でtry catch構文にて囲った場合、原因の元となるエラーを深掘りするにcause フィールドが役立ちます。

async function start() {
  try {
    await load();
  } catch (error) {
    console.log(error);
    // console.log(error.cause); // さらに奥の情報を追跡できる
  }
}

async function load() {
  try {
    const result = await loadJson();
    console.log(result);
  } catch (error) {
    throw new Error("読み込みに失敗!", { cause: error });
  }
}

async function loadJson() {
  let data;
  try {
    data = await fetch("example.json");
  } catch (error) {
    // ネットワークがオフラインの場合
    throw new Error("fetchに失敗しました。", { cause: error });
  }

  if (data.ok === false) {
    // 404 の場合等(この場合は、明示的なエラーなのでcause未指定)
    throw new Error("ファイルの読み込みに失敗しました。");
  }

  let json;
  try {
    json = await data.json();
    return json;
  } catch (error) {
    // JSONのパースに失敗
    throw new Error("JSONデータの展開に失敗しました。", { cause: error });
  }
}

▼ネットワークがオフライン状態であり、fetch()メソッドでエラーが起きる場合

▼該当ファイルがウェブサーバーに存在せず、ステータス404で読み込めない場合

▼ファイルを受信したが、JSONのパースに失敗する場合

2022年6月現在は、Firefoxだとコンソールパネルでエラーのcauseを自動的に展開してくれます。ChromeやSafariでは一階層目しかエラーがでてこないで、自前でcauseプロパティーを追跡する必要があります。

参考文献

RegExp Match Indices (/dフラグ)

正規表現にマッチインデックス機能(/dフラグ)が追加されます。このオプションを指定すると、ひっかかった部分文字列の先頭と末尾のインデックスについての追加情報が得られます。

const text = "今日の夕食代:500円";
const regexp = /今日の夕食代:(?<digit>\d{3})円/dg;

for (const match of text.matchAll(regexp)) {
  console.log(match);
}

結果は次の通りです。

[
  '今日の夕食代:500円',
  '500',
  index: 0,
  input: "今日の夕食代:500円",
  groups: { digit: '500' },
  indices: {
    [ 0, 11 ],
    [ 7, 10 ],
    groups: { 
      digit: [7, 10]
    }
  }
]

注意点として、トランスパイルできず、/dフラグは古いブラウザでは動作しません。ポリフィル『regexp-match-indices』が存在しますが、ES2022の書き方とは異なります。

参考文献

対応環境の状況

ウェブブラウザの対応状況

本記事で紹介したES2022の仕様は、次のブラウザや実行環境で対応しています。2022年6月時点の現行ブラウザで対応しているものには「◯」を、対応開始バージョンを記載しています。

機能 Chrome Firefox Safari Node.js
.at() ◯ 92 ◯ 90 ◯ 15.4 ◯ 16.6
Error.cause ◯ 93 ◯ 91 ◯ 15 ◯ 16.9
Object.hasOwn ◯ 93 ◯ 92 ◯ 15.4 ◯ 16.9
Top-Level Await ◯ 89 ◯ 89 ◯ 15 ◯ 14.8
Class Private Features ◯ 74 ◯ 90 ◯ 14.1 ◯ 12.0
Class Static Fields ◯ 49 ◯ 45 ◯ 9 ◯ 6.0
RegExp: Match Indices ◯ 90 ◯ 88 ◯ 15 ◯ 16

ES2022の各機能に対応していない古いブラウザバージョンに対応するには、トランスパイルをする必要があります。ただ、新しい記法が下位のECMAScriptにトランスパイルできなくなってきているのも事実です。たとえばクラスの#は、TypeScriptのコンパイルではES2015が最下位バージョンとなります。

IE11が今月2022年6月15日にサポート切れとなりますが、新しいECMAScriptの恩恵を受けつつ、対象ブラウザの下限をどこに設定するか、JavaScriptの出力ターゲットの再検討が必要な時期に来ていると思います。

TypeScript・Babelの環境構築については、次の記事で詳しく紹介しています。

まとめ

今回紹介したES2022の新機能のうち、クラスの進化が興味深く感じました。Angularは別として、ReactやVueではクラスとは異なる方向性(関数コンポーネントや、Composition API)で利用されています。クラスが求められていた時代から遅れてES2022にクラス仕様が固まったのは、興味深い進化と言えそうです。プライベートフィールドが他の言語で見られない#であることに、多くの議論がありましたが、ES2022として着地できたことを嬉しく思います。

新仕様はこれまで面倒だった処理を簡潔にしてくれる一面もあります。実行環境が揃うまで待ってから採用するか、トランスパイルやポリフィルを利用して早めに導入するか、この機会に検討してみてはいかがでしょうか。