とほほのES2024入門
ES2024とは
JavaScript の標準仕様 ES2024 (ECMAScript 15th Edision) として 2024年6月にリリースされました。ES2024 で新たに追加された機能について説明していきます。
- https://www.ecma-international.org/ecma-262/
- https://github.com/tc39/proposals/blob/master/finished-proposals.md
ES2024強化項目
Well-Formed Unicode Strings
文字コードの適切さを判断・置換する string.isWellFormed() と string.toWellFormed() が追加されました。
string.isWellFormed() は文字列が適切な文字コードで構成されているかを判断します。例えば、サロゲートペア領域の文字「𩸽(ホッケ)(U+29E3D)」はサロゲートペアで表現すると U+D867 と U+DE3D の2文字になりますが、1文字目のみだったり、2文字目のみだったりした場合に false を返します。
"𩸽".isWellFormed() // true "\uD867\uDE3D".isWellFormed() // true "\uD867".isWellFormed() // false "\uDE3D".isWellFormed() // false
string.toWellFormed() は文字列が不正な文字コードを含んでいる場合、その文字を置換文字(Replacement Character) と呼ばれる � (U+FFFD) に置換します。
"\uD867".toWellFormed() // "\uFFFD" "\uDE3D".toWellFormed() // "\uFFFD"
RegExp v flag with set notation + properties of strings
正規表現のフラグで u に加えて v が追加されました。
結合文字への対応
ES2015 では正規表現フラグに u が追加され、サロゲートペア領域の文字も扱えるようになりました。例えば 😵 (U+1F635) はサロゲートペアを用いると U+D83D U+DE35 の2文字になりますが、u フラグを用いることにより1文字として認識できるようになります。
"\uD83D\uDE35".match(/^.$/) // 2文字と判断されてマッチしない "\uD83D\uDE35".match(/^.$/u) // 1文字と判断されてマッチする
また、ES2018 では正規表現で \p{...} の表現が使用可能となりました。
"\uD83D\uDE35".match(/^\p{Emoji}*$/u) // 絵文字と判断されてマッチする
しかし、😵(U+1F635) と 💫(U+1F4AB) をゼロ幅結合子(U+200D) で結合して 😵💫(U+1F635 U+200D U+1F4AB) とするなどの 結合文字 まではサポートされていませんでした。
U+1F635
U+200D
U+1F4AB
U+1F635 U+200D U+1F4AB
u フラグの代わりに v フラグを用いることで、上記ような結合文字に対してもうまくマッチできるようになります。
"\uD83D\uDE35\u200D\uD83D\uDCAB".match(/^\p{Emoji}$/u) // うまくマッチしない
"\uD83D\uDE35\u200D\uD83D\uDCAB".match(/^\p{RGI_Emoji}$/v) // うまくマッチする
プロパティ名は Emoji の代わりに下記などを使用します。(詳細)
- Basic_Emoji
- Emoji_Keycap_Sequence
- RGI_Emoji
- RGI_Emoji_Modifier_Sequence
- RGI_Emoji_Flag_Sequence
- RGI_Emoji_Tag_Sequence
- RGI_Emoji_ZWJ_Sequence
差集合・積集合・ネスト
[A--B] は A から B を除いた差集合を意味します。下記では ASCII 文字から数字を除外した文字にマッチします。
"5".match(/[\p{ASCII}]/v) // "5" はASCIIなのでマッチする
"A".match(/[\p{ASCII}--\p{Number}]/v) // ASCIIであっても数字はマッチしない
[A&&B] は A であり、かつ B である積集合を意味します。下記では数字であり、かつ、ASCII 文字である文字にマッチします。
"5".match(/[\p{Number}]/v) // 全角文字でも数字なのでマッチする
"5".match(/[\p{Number}&&\p{ASCII}]/v) // 数字であってもASCIIではないのでマッチしない
下記の様に [...] をネストできるようになります。
"A".match(/[[A-Z][a-z][0-9]]/v) // A-Z と a-z と 0-9 "A".match(/[\p{ASCII}--[0-9]]/v) // ASCII から 0-9 を除いたもの
Array Grouping
配列やオブジェクトをグルーピングするメソッド Object.groupBy() と Map.groupBy() が追加されました。
Object.groupBy()
下記の例は [1, 2, 3, 4, 5] の配列を奇数(odd)と偶数(even)にグルーピングします。
const array = [1, 2, 3, 4, 5];
Object.groupBy(array, (num, index) => {
return num % 2 === 0 ? 'even': 'odd';
});
// => { odd: [1, 3, 5], even: [2, 4] }
下記の例はメンバリストを address でグルーピングします。
const members = [
{ name: "Yamada", address: "Tokyo" },
{ name: "Suzuki", address: "Osaka" },
{ name: "Tanaka", address: "Nagoya" },
{ name: "Kaneda", address: "Tokyo" }
];
Object.groupBy(members, x => x.address)
// {
// Tokyo: [ { name: "Yamada", address: "Tokyo" }, { name: "Kaneda", address: "Tokyo" } ],
// Osaka: [ { name: "Suzuki", address: "Osaka" } ],
// Nagoya: [ { name: "Tanaka", address: "Nagoya" } ]
// }
本来は Array.prototype.groupBy() として実装したいところですが、すでに同名の関数を拡張しているライブラリが出回っているため、Object.groupBy() という静的メソッドとして定義されることになりました。
Map.groupBy()
Map.groupBy() はグルーピングの結果を Map で返します。
const members = [
{ name: "Yamada", address: "Tokyo" },
{ name: "Suzuki", address: "Osaka" },
{ name: "Tanaka", address: "Nagoya" },
{ name: "Kaneda", address: "Tokyo" }
];
Map.groupBy(members, x => x.address)
// Map {
// Tokyo => [ { name: "Yamada", address: "Tokyo" }, { name: "Kaneda", address: "Tokyo" } ],
// Osaka => [ { name: "Suzuki", address: "Osaka" } ],
// Nagoya => [ { name: "Tanaka", address: "Nagoya" } ]
// }
Object.groupBy() の場合はキーが文字列である必要がありますが、Map.groupBy() を使用すると、数値やオブジェクトをキーとするマップを扱うことが可能となります。
const members = [
{ name: "Yamada", age: 26 },
{ name: "Suzuki", age: 32 },
{ name: "Tanaka", age: 45 },
{ name: "Kaneda", age: 26 }
];
const result = Map.groupBy(members, x => x.age);
console.log(result.get(26)); // [ { name: "Yamada", age: 26 }, { name: "Kaneda", age: 26 } ]
console.log(result.get(32)); // [ { name: "Suzuki", age: 32 } ]
console.log(result.get(45)); // [ { name: "Tanaka", age: 45 } ]
Resizable and growable ArrayBuffers
ArrayBuffer がリサイズできるようになりました。生成時に { maxByteLength: n } を指定することで最大 n バイトまでリサイズすることが可能です。.resizable はリサイズ可能か否か、.maxByteLength は最大バイトサイズを返却します。
const buf = new ArrayBuffer(8, { maxByteLength: 16 });
console.log(buf.resizable); // true
console.log(buf.maxByteLength); // 16
console.log(new Uint32Array(buf).byteLength); // 8
buf.resize(16);
console.log(new Uint32Array(buf).byteLength); // 16
buf.resize(32); // Uncaught RangeError
ArrayBuffer transfer
ArrayBuffer.transfer(n) および ArrayBuffer.transferToFixedLength(n)が追加されました。ArrayBuffer のサイズを n バイトに変更し、新しく確保した ArrayBuffer を返却します。元データもコピーされ、短くなる場合は切捨てられ、長くなる場合は 0 が埋められます。.transfer() は元のバッファのリサイズ可否を継承します。.transferToFixedLength() はリサイズ不可なバッファを返します。
const buf1 = new ArrayBuffer(8, { maxByteLength: 32 });
console.log(buf1.byteLength); // 8
console.log(buf1.resizable); // true
const buf2 = buf1.transfer(16);
console.log(buf2.byteLength); // 16
console.log(buf2.resizable); // true
const buf3 = buf2.transferToFixedLength(32);
console.log(buf3.byteLength); // 32
console.log(buf3.resizable); // false
Atomics.waitAsync()
Atomics.waitAsync() は Atomics.wait() の非同期版で、Web Worker や Service Worker との間で共有メモリを使用する際、共有メモリの内容が変化するのを非同期で待ち受けることができます。
result = Atomics.waitAsync(typedArray, index, value) result = Atomics.waitAsync(typedArray, index, value, timeout)
typedArray は SharedArrayBuffer に紐づけられた Int32Array または BigInt64Array です。index は先頭からの位置、value は検査値、timeout は省略可能でタイムアウト時間をミリ秒で指定します。waitAsync() は typedArray の先頭から index 番目のメモリの値が検査値 value と合致しているかを調べ、その結果により下記のオブジェクトを返します。
- 検査値が既に異なる場合:
{ async: false, value: "not-equal" } - 検査値が等しく、
timeoutが0の場合:{ async: false, value: "timed-out" } - 検査値が等しく、
timeoutが0以外の場合:{ async: true, value: Promise }
Promise が履行(fulfilled)状態になると下記の値を返します。
- 値が変化した場合:
"ok" timeoutミリ秒待っても値が変化しなかった場合:"timed-out"
Web Worker で使用する例を下記に示します。
const sab = new SharedArrayBuffer(4);
const int32 = new Int32Array(sab);
const worker = new Worker("worker.js");
worker.postMessage(sab);
setTimeout(() => {
Atomics.store(int32, 0, 1);
Atomics.notify(int32, 0);
console.log("Main: Change data");
}, 2000);
self.onmessage = async (event) => {
const int32 = new Int32Array(event.data);
console.log("Worker: 待機開始");
const result = Atomics.waitAsync(int32, 0, 0, 3000);
console.log(`async: ${result.async}`); // true or false
if (result.async) {
result.value.then((value) => {
console.log(`value: ${value}`); // "ok" or "timed-out"
});
} else {
console.log(`value: ${result.value}`); // "not-equal" or "timed-out"
}
};
Promise.withResolvers()
Promise を返却する関数を定義する場合、たとえば下記の様に記述します。
async function request(url) {
const promise = new Promise(async (resolve, reject) => {
try {
const resp = await fetch(url);
const body = await resp.text();
resolve(body);
} catch (err) {
reject(err);
}
});
return promise;
}
Promise.withResolvers() を用いることにより、上記のネストをひとつ浅くすることができます。resolve や reject を new Promise(...) のスコープ外で使用でる点もメリットです。
async function request(url) {
const { promise, resolve, reject } = Promise.withResolvers();
try {
const resp = await fetch(url);
const body = await resp.text();
resolve(body);
} catch (err) {
reject(err);
}
return promise;
}