echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【JavaScript】ES2022で正式採用される機能について

本題

JavaScriptの仕様であるECMAScript(=ES)について、そろそろ最新版のES2022が承認される、というのをICSさんのブログ(「参考サイト」参照)で知りました。

なので今回は、ES2022で正式採用される機能について、コード例を挙げて説明したいと思います。

なお今回紹介する機能ですが、Node.jsの場合、Node Greenを見るとわかる通り、2022/6/18現在のLTSであるver16.15.1では、普通に全機能が使用可能です。(ブラウザでもほとんどの機能は使用可能の模様)

参考サイト

紹介する機能一覧

  • クラス以外の機能
    • top-level await
    • .at() method on the built-in indexables
    • Object.hasOwn
    • Error.cause property
    • RegExp Match Indices
  • クラス関連
    • フィールド・メソッド・アクセスレベル
      • クラスフィールド、static、private
    • Ergonomic brand checks for private fields

クラス以外の機能

top-level await

モジュールの最上位ソースに限り、asyncなどをつけなくても、直接awaitをつけることができるようになりました。
これにより最上位ソースで非同期処理を扱いやすくなりました。

なお、top-level awaitはモジュール(※)限定なので、そうではないJSソースで実行してもエラーになります。
またあくまで最上位ソース限定なので、そうではない場所(例えば関数内など)で実行する場合は、今まで通りasync定義が必要です。

※下記に該当するソ-ス

  • 拡張子*.mjsのファイル
  • package.json"type":"module"が定義されたファイル
  • <script type="module"> が定義されたHTMLスクリプト
import * as axios from 'axios';

// モジュールファイルの最上位のみ、asyncが不要。  
// 下記ソースは正常動作する。
const x = await axios.default.get("https://hogehoge.com/users/1");
console.log(JSON.stringify(x.data));  
  
// 関数内など、トップレベル以外ではasyncが必要。  
// なので下記ソースはエラーになる。 
function badExampleFuncAsync() {
  const x = await axios.default.get("https://hogehoge.com/users/1");
  console.log(JSON.stringify(x.data));  
}  
  

.at() method on the built-in indexables

String及びArray, TypedArrayの要素に、.at(index)でindexの要素にアクセスできます。
「indexの要素にアクセス」自体は元からあるのですが、substr関数のように、負数を指定することで末尾からのindexを指定できます。

これにより、例えば配列の末尾要素を指定する際に、今までのようなarray[array.length -1]というソースは不要になります。

const str = 'hello,es2022';
const array = ['hello', 'es', '2022'];
  
// これは今までと同じ。
console.log(str.at(6));  // e
console.log(array.at(2));  // 2022  
  
// 負数を指定することで、末尾からの要素を指定できる。  
// (-1が末尾要素のindex)
console.log(str.at(-8));  // o
console.log(array.at(-3));  // hello

Object.hasOwn

オブジェクトが該当プロパティを持っているかを判定できる関数です。
というか実際にはObject.prototype.hasOwnProperty.callのショートハンドになります。

ただhasOwnPropertyにはプロトタイプ汚染の問題がある&上記が長くて煩わしいので、それを防ぐ目的だと思われます。

プロトタイプ汚染については、こちらのサイトで詳しく記載されています。

const obj = {
  x: 10,
  y: 20,
};
  
console.log(Object.hasOwn(obj, "x"));  // true
console.log(Object.hasOwn(obj, "z"));  // false

Error.cause property

親子関係(≒依存関係)があるソースでエラーが発生した際、エラーの発生源をたどれるようになる仕組みです。
これはなかなか文章で説明しにくいので、具体的なソースを見た方が早いと思います。

function parent() {
    try {
        child();
    } catch (e) {
        // ここのeはchild()からthrowされたもの。  
        console.log(`e is ${e.message}`);    
        console.log(e.cause.message);     
        console.log(e.cause.cause.message);
    }
}
  
function child() {
    try {
        grandChild();
    } catch (e) {  
        // ここのeはgrandChild()からthrowされたもの。
        // 発生源にe(=grandChild Error)を指定
        throw new Error("child Error", {cause: e});
    }
}
  
function grandChild() {
    try {
        nextGrandChild();
    } catch (e) {
        // ここのeはnextGrandChild()からthrowされたもの。
        // 発生源にe(=nextGrandChild Error)を指定
        throw new Error("grandChild Error", {cause: e});
    }
}
  
function nextGrandChild() {
    // ここがエラー発生源
    throw new Error("nextGrandChild Error");
}
  
parent();

上記ソースを実行すると、下記ログが出力されます。(不要な部分はカット)

// childでthrowされたエラーのメッセージ
e is child Error  
  
// childでthrowされたエラーのcauseに設定されたエラー  
// (=grandChildでthrowされたエラー)のメッセージ
grandChild Error  
  
// grandChildでthrowされたエラーのcauseに設定されたエラー  
// (=nextGrandChildでthrowされたエラー)のメッセージ
nextGrandChild Error

RegExp Match Indices

正規表現でのマッチングについて、dフラグを付けることで、一致した位置の情報を付加することができるようになりました。
一致位置情報は戻り値のindicesプロパティで確認できます。

const result = /cd/d.exec('abcdefabcdefg');
console.log(result);

const result2 = /cd(ef)/d.exec('abcdefabcdefg');
console.log(result2);

上記の実行結果がこちら。

// resultの結果
[
  'cd',
  index: 2,
  input: 'abcdefabcdefg',
  groups: undefined,
  indices: [ [ 2, 4 ], groups: undefined ]
]
  
// result2の結果
[
  'cdef',
  'ef',
  index: 2,
  input: 'abcdefabcdefg',
  groups: undefined,
  indices: [ [ 2, 6 ], [ 4, 6 ], groups: undefined ]
]

indicesのindex[0]に、一致位置の開始位置&終了位置が配列で入ってきます。
なお終了位置はsilce関数で指定するものと同じで、「一致文字列の最終文字のindex+1」となります。
(resultの例(=「cd」)で言えば、「d」のindexである3ではなく、4になる)

indicesのindex[1]以降では、キャプチャグループがある場合にキャプチャグループとの一致位置の開始位置&終了位置が配列入ってきます。
今回の場合、result2でキャプチャグループ「ef」の一致位置情報である配列[4, 6]が返ります。

クラス関連

ここからは、クラス関連の機能になります。
ES2022はクラス関連の機能が多く、やっとクラスの基本的な機能が多言語に追いついたという印象です。

フィールド・メソッド・アクセスレベル(まとめて説明)

クラスの基本的な要素であるフィールド・メソッド・アクセスレベルについて、下記機能が追加されました。

  • クラスフィールドを設定可能になった。
    • メソッド同様、静的(static)フィールドも設定可能
  • クラスメソッドに静的メソッドを設定可能になった
  • privateなフィールド/メソッドを設定可能になった
    • 当然、クラス外からはアクセス不可
    • これでやっと現場レベルのルールで設定する必要がなくなる
class Sample {
    x = 1;  
    // 変数名の前に#をつけると、privateなフィールド/メソッドになる。
    #y = 2;
    
    // staticをつけると、静的フィールド/メソッドになる。
    static sx = 10;
    static #sy = 20;
    
    constructor() {}
  
    // getter  
    // ちなみにgetter/setterもprivateに設定可能。  
    // ただ、利用機会は少ないかも...
    getY() {
        return this.#y;
    }
    
    // privateフィールド&メソッドは、クラス内からのみ参照可能
    getprivateMethodValue() {
        return this.#privateMethod();
    }
    
    // static/privateは、もちろんメソッドにも設定可能
    #privateMethod() {
        return 'I am private method';
    }
    
    static staticPublicFunc(){
        return this.#staticPrivateFunc();
    };
    
    static #staticPrivateFunc(){
        return this.#sy;
    }
}

  
const cls = new Sample();
console.log(cls.x);  // 1
console.log(cls.#y);  // エラー
console.log(cls.getY());  // 2
  
console.log(Sample.sx);  // 10
Console.log(Sample.#sy);  // エラー
  
console.log(cls.#privateMethod());  // エラー
console.log(cls.getprivateMethodValue());  // I am private method
console.log(Sample.#staticPrivateFunc());  // エラー
console.log(Sample.staticPublicFunc());  // 20

なおVSCodeでは、プライベートフィールド/メソッドに外部からアクセスしようとしたところ、ちゃんと怒られました。

Ergonomic brand checks for private fields

「人間工学に基づくプライベートフィールドのブランドチェック」ってなんのこっちゃ?という感じですが、オブジェクトのinプロパティを使って、プライベートフィールド&メソッドを持っているかをクラスレベルで調べられる機能のことらしいです。

これもソースを見た方が早いので、ソースをどうぞ。

class Sample {
    #y = 2;
    constructor(y = null) {
        if(y ?? null) this.#y = y;
    };

    static isSample(obj){  
        // #y in objで、obj内のプライベートフィールド#yを評価する。  
        // なお#yは正確にはthis.#yなので「Sampleクラスの#yフィールドを
        // 持っているかどうか」を判定する。
        // ここではprivateフィールドで行っていますが、privateメソッド及び  
        // 両方を組み合わせることも可能です。  
        return #y in obj;
    }
}

class Sample2 {
    #y = 2;
    constructor() {}
}  
  
// true(クラスが同じなので)
console.log(Sample.isSampleByField(new Sample()));  
  
// false(クラスが違うので)
console.log(Sample.isSampleByField(new Sample2()));  
  
// true(クラスが同じなので。#yの値そのものは問わない)
console.log(Sample.isSampleByField(new Sample(10))); 

感想

今回の感想としては、

  • クラス以外の機能
    • あればあるで便利な機能が増えた
  • クラス関連の機能
    • 基本的な機能が多言語に追いついた

という感じです。

ただtop-level awaitや.at()は地味ですが個人的にはうれしい機能だなと思いました。

またクラスに関しては、やっと普通に使えるようになったなあ...という感じです。(protectedとかあるともっといいんですけどね)

それでは、今回はこの辺で