とほほの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; }