@mizumotokのブログ

テクノロジー、投資、書評、映画、筋トレなどについて

JavaScript ES2024の新機能

ECMAScriptJavaScriptの標準規格で、2015年以降毎年改定されています。この記事ではES2024に採用された仕様、ES2025に決まっている仕様、次に盛り込まれそうな仕様について解説します。

ECMAScriptとは

ECMAScriptとはJavaScriptの標準規格

ECMAScript(エクマスクリプトと読む)とはJavaScriptの標準規格です。 ECMAScriptの背景や策定プロセスについては以前のブログを参考にしてください。

mizumotok.hatenablog.jp

ECMAScriptのバージョン

ECMAScriptのバージョンはedition番号で管理されていて、例えば第5版はES5と呼ばれます。ES6からは毎年仕様が改定されており、ES2015のように発行年つきで呼ぶことが推奨されています。ES6とES2015、ES7とES2016はそれぞれ同じ仕様です。

バージョン 年号付きバージョン 公開日
ES 1997年6月
ES2 1998年6月
ES3 1999年6月
ES4 放棄
ES5 2009年12月
ES5.1 2011年6月
ES6 ES2015 2015年6月
ES7 ES2016 2016年6月
ES8 ES2017 2017年6月
ES9 ES2018 2018年6月
ES10 ES2019 2019年6月
ES11 ES2020 2020年6月
ES12 ES2021 2021年6月
ES13 ES2022 2022年6月
ES14 ES2023 2023年6月
ES15 ES2024 2024年6月
ES16 ES2025 公開前

ECMAScriptの策定プロセス

ES2015以降では、仕様の提案は5段階のステージに分けられ、リリース時期(毎年6月頃)にステージ4にあるものがリリース対象になります。

ステージ 状態 説明
0 Strawperson 仕様の提案
1 Proposal - 変更を適用したいケースの作成
- 解決方法の説明
- 潜在的な変更の適用
2 Draft 文法やその意味を言語仕様のフォーマットで記述
3 Candidate 実装とユーザからのフィードバック待ちの状態
4 Finished 仕様に追加できる状態

ES2024

Finishedのステージにあるものから発行年度が2024年のものがES2024になります。

ArrayBuffer transfer

ArrayBufferに2つのメソッドと1つのプロパティが追加されました。

class ArrayBuffer {
  // ... その他のメソッド等

  // 同じバイト配列を持つArrayBufferを返して、
  // 元のArrayBufferをdetacheする
  transfer(newByteLength);

  // サイズ変更不可能なArrayBufferを返す以外はtransferと同じ
  transferToFixedLength(newByteLength);

  // ArrayBufferがdetacheされているかどうか
  get detached();
}

ArrayBufferはメモリの参照にすぎないので、以下の例のようにタイミングによってはデータが変更されている可能性があります。

function validateAndWrite(arrayBuffer) {
  // 非同期なvalidationをする
  await validate(arrayBuffer);

  // validationが通ったらファイルに書き込む
  await fs.writeFile("data.bin", arrayBuffer);
}

const data = new Uint8Array([0x01, 0x02, 0x03]);
validateAndWrite(data.buffer);
setTimeout(() => {
  data[0] = data[1] = data[2] = 0x00;
}, 50);

こういった場合の対処として、今まではコピーしてつかっていました。

function validateAndWriteSafeButSlow(arrayBuffer) {
  // まずコピーする
  const copy = arrayBuffer.slice();

  await validate(copy);
  await fs.writeFile("data.bin", copy);
}

上記のコードはコピーするのに時間がかかるし、メモリが倍必要になるという問題がありました。 そこでtransferメソッドが導入されました。

function validateAndWriteSafeAndFast(arrayBuffer) {
  // 所有権を移動(コピーされたのと同じだが、元のarrayBufferはdetacheされている)
  const owned = arrayBuffer.transfer();

  // arrayBufferはすぐにdetacheされる(メモリから切り離されている)
  assert(arrayBuffer.detached);

  await validate(owned);
  await fs.writeFile("data.bin", owned);
}

transferを使用することで、コピーの時間や倍のメモリを必要とせずに安全に処理ができるようになります。

Promise.withResolvers

Promiseをつくるとき、resolveとrejectを引数とするとコールバック関数を引数として渡します。

const promise = new Promise((resolve, reject) => {
  asyncRequest(config, response => {
    const buffer = [];
    response.on('data', (data) => buffer.push(data));
    response.on('end', () => resolve(buffer));
    response.on('error', reason => reject(reason));
  });
});

promise
  .then((buffer) => nextProcess(buffer))  // promiseが完了したらnextProcess関数を呼ぶ
  .catch((reason) = console.log(reason);

しかし、例えばresponseのcallback-requestイベントでendイベントの完了を待ってnextProcess関数を呼びたいとしたらどうなるでしょう?
callback-requestイベントのコールバック関数内で、endイベント完了のPromiseを呼び出す必要があります。
この場合、以下のようにPromiseを先に作っておいて後からPromiseのコールバック関数を定義します。

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});
asyncRequest(config, response => {
  const buffer = [];
  response.on('callback-request', id => {
    promise.then(data => nextProcess(id, data));  // callback-requestイベントがいつ来ても、endイベントの完了を待って処理する
  });
  response.on('data', data => buffer.push(data));
  response.on('end', () => resolve(buffer));
  response.on('error', reason => reject(reason));
});

このようにイベントの待ち合わせをするケース等、Promiseをインスタンス化した後でresolve/rejectを使いたいという場合があります。
その場合に使えるのがPromise.withResolvers()で、具体的には上記のコードの最初の5行をまとめてやってくれるメソッドです。

const { promise, resolve, reject } = Promise.withResolvers();
 
// 以下のコードと同じ
// let resolve, reject;
// const promise = new Promise((res, rej) => {
//   resolve = res;
//   reject = rej;
// });

Array Grouping

ObjectとMapにgroupByメソッドが追加されました。
それぞれArrayを分類して、ObjectやMapに変換して返してくれます。

const array = [1, 2, 3, 4, 5];

// `Object.groupBy` は任意のキーで分類します
Object.groupBy(array, (num, index) => {
  return num % 2 === 0 ? 'even': 'odd';
});
// =>  { odd: [1, 3, 5], even: [2, 4] }

// `Map.groupBy` はMapで返し、キーにオブジェクトが使えます
// using an object key.
const odd  = { odd: true };
const even = { even: true };
Map.groupBy(array, (num, index) => {
  return num % 2 === 0 ? even: odd;
});
// =>  Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }

ArrayBufferのサイズ変更機能

ArrayBuffer のサイズを動的に変更することができるようになりました。コンストラクタでmaxByteLengthを指定することで変更可能となります。

const buffer = new ArrayBuffer(8, { maxByteLength: 16 });  // 最大16バイトまで拡張可能な8バイトのバッファを作成
buffer.resize(12);  // バッファのサイズを12バイトに変更
console.log(buffer.resizable);  // true
console.log(buffer.maxByteLength);  // 16

WebAssembly(Wasm)では、メモリが ArrayBuffer を介して管理されます。従来、Wasm のメモリを拡張する際には新しい ArrayBuffer を作成し、古いものをデタッチする必要がありました。しかし、ES2024 の新機能により、ArrayBuffer をインプレースでリサイズできるため、メモリ管理がより効率的かつ簡潔になりました。

// Wasm メモリのバッファを取得
let wasmMemoryBuffer = wasmInstance.exports.memory.buffer;

// サイズ変更可能か確認
if (wasmMemoryBuffer.resizable) {
  // 必要に応じてサイズを拡張
  wasmMemoryBuffer.resize(newSize);
}

このように、ArrayBuffer の新機能を活用することで、Wasm と JavaScript 間のメモリ操作がよりシームレスになります。

SharedArrayBufferも同様にサイズ拡張が可能になっています。

const sab = new SharedArrayBuffer(1024, { maxByteLength: 2048 });
console.log(sab.maxByteLength); // 2048
console.log(sab.growable); // true
sab.grow(512); // 現在のサイズから512バイト増加
console.log(sab.byteLength); // 1536

RefExp vフラグ

RefExpにvフラグが追加されました。ES2015でuフラグが追加されて正規表現がコードポイントで指定できるようになりましたが、それを拡張するものになります。

(1) Properties of Stringsの対応

const regexGreekSymbol = /\p{Script_Extensions=Greek}/u;
regexGreekSymbol.test('π');
// → true

ES2018のUnicode 文字クラスエスケープ\pUnicodeプロパティを使って以下のような正規表現がつくれるのですが、絵文字は複数のコードポイントをもつことがあるのでうまくいきません。

// Emoji = 絵文字のUnicodeプロパティ
const re = /^\p{Emoji}$/u;

// コードポイントが1つの場合
re.test('⚽'); // '\u26BD'
// → true ✅

// 複数のコードポイントを持つ場合
re.test('👨🏾<200d>⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → false ❌

Unicode にはいくつかの Properties of Stringsが定義されているので、これをvフラグで取り入れました。

// RGI_Emoji = 絵文字のUnicode Properties of Strings
const re = /^\p{RGI_Emoji}$/v;

re.test('⚽'); // '\u26BD'
// → true ✅

re.test('👨🏾<200d>⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → true ✅

(2) 論理演算

これまでのUnicodeプロパティと今回対応されたProperties of Stringsに対して、論理演算が行えるようになりました。

// 交差 Intersection &&
const regex = /[\p{Alphabetic}&&\p{Number}]/v;
console.log(regex.test("A")); // false(Aはアルファベットだが数字ではない)
console.log(regex.test("1")); // false(1は数字だがアルファベットではない)

// 差分 Subtraction --
const regex = /[\p{Script=Latin}--\p{Number}]/v;
console.log(regex.test("A")); // true(Aはラテン文字)
console.log(regex.test("1")); // false(1はラテン文字に含まれるが、Number にも該当するので除外される)

// 和集合 Union ||
const regex = /[\p{Script=Latin}||\p{Script=Greek}]/v;
console.log(regex.test("A")); // true(ラテン文字)
console.log(regex.test("Γ")); // true(ギリシャ文字)
console.log(regex.test("あ")); // false(ひらがなはマッチしない)

Atomics.waitAsync

Atomics.waitの非同期版で共有メモリ上の特定の位置で非同期的に待機するために使用されます。メインスレッドのように(Atomics.wait() のような)ブロッキング操作が許可されていない環境で有用です。

メインスレッド側

// 共有メモリの作成(4バイト = Int32 1つ)
const sharedBuffer = new SharedArrayBuffer(4);
const int32Array = new Int32Array(sharedBuffer);

// Worker を作成
const worker = new Worker("worker.js");
worker.postMessage(sharedBuffer);

// 非同期で待機
const result = Atomics.waitAsync(int32Array, 0, 0);
if (result.async) {
  result.value.then((status) => {
    console.log("Workerからの通知:", status); // "ok" が出力される
    console.log("計算結果:", int32Array[0]); // 計算後の値を取得
  });

// この後も処理は続けられる

Worker側

self.onmessage = (event) => {
  const sharedBuffer = event.data;
  const int32Array = new Int32Array(sharedBuffer);

  // 何らかの計算処理(例: 数値を更新)
  setTimeout(() => {
    int32Array[0] = 42; // 計算結果を格納
    console.log("Worker: 計算完了");
    Atomics.notify(int32Array, 0); // メインスレッドを起こす
  }, 2000);
};

実行結果

(2秒後)
Worker: 計算完了
Workerからの通知: ok
計算結果: 42

文字列が「正しい」コードかの判定と修正

JavaScriptの文字列は、16ビットの値(コードユニット)のシーケンスとして表現されます。しかし、すべての16ビット値の組み合わせが有効なUnicode文字を構成するわけではありません。特に、サロゲートペアと呼ばれる特定の範囲のコードユニットは、正しい組み合わせで使用されないと「不正な(ill-formed)」文字列となります。

サロゲートペアとは:

これらは単独では有効なUnicode文字を表さず、リードサロゲートとテールサロゲートが対になって初めて1つのUnicode文字を表現します。したがって、サロゲートペアが正しく組み合わされていない文字列は「不正な」ものと見なされます。

Stringクラスに文字列正しいか判定するisWellFormedメソッドと不正なサロゲートUnicodeの置換文字(U+FFFD:�)に置き換えるtoWellFormedが追加されました。

const str1 = 'Hello\uD83D\uDE00'; // 正しいサロゲートペアを含む
console.log(str1.isWellFormed()); // true

const str2 = 'Hello\uD83D'; // 不正なサロゲートを含む
console.log(str2.isWellFormed()); // false


const str = 'Hello\uD83D'; // 不正なサロゲートを含む
const wellFormedStr = str.toWellFormed();
console.log(wellFormedStr); // 'Hello�'

まとめ

  • ArrayBuffer転送・サイズ変更機能により、メモリ管理が効率化され、高速なデータ移動や動的リサイズが可能になりました。
  • Promise.withResolvers()Atomics.waitAsync()により、非同期処理やスレッド間の同期が簡潔に記述できるようになりました。
  • 文字列処理と正規表現の強化isWellFormed()toWellFormed()v フラグ)により、Unicode の正規性チェックや柔軟なパターンマッチングが可能になりました。