JavaScript ES2022 / ES2023(予定される機能)
ECMAScriptはJavaScriptの標準規格で、2015年以降毎年改定されています。この記事を読むとES2022に採用された仕様、ES2023に決まっている仕様、次に盛り込まれそうな仕様がわかります。
- ECMAScriptとは
- ES2022
- ES2023(確定分)
- 現在のステージ3(2022年8月15日現在)
- Legacy RegExp features in JavaScript
- Atomics.waitAsync
- Import Assertions
- JSON modules
- Temporal
- Resizable and growable ArrayBuffers
- ShadowRealms
- Array Grouping
- Decorators
- RegExp v flag with set notation + properties of strings
- Change Array by Copy
- Symbols as WeakMap keys
- JSON.parse source text access
- Duplicate named capturing groups
- まとめ
ECMAScriptとは
ECMAScriptとはJavaScriptの標準規格
ECMAScript(エクマスクリプトと読む)とはJavaScriptの標準規格です。 ECMAScriptの背景や策定プロセスについては以前のブログを参考にしてください。
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 | 公開前 |
ECMAScriptの策定プロセス
ES2015以降では、仕様の提案は5段階のステージに分けられ、リリース時期(毎年6月頃)にステージ4にあるものがリリース対象になります。
ステージ | 状態 | 説明 |
---|---|---|
0 | Strawperson | 仕様の提案 |
1 | Proposal | - 変更を適用したいケースの作成 - 解決方法の説明 - 潜在的な変更の適用 |
2 | Draft | 文法やその意味を言語仕様のフォーマットで記述 |
3 | Candidate | 実装とユーザからのフィードバック待ちの状態 |
4 | Finished | 仕様に追加できる状態 |
ES2022
Finishedのステージにあるものから発行年度が2022年のものがES2022になります。
.at()
配列の負の添字を可能にします。
配列の最後をとるのにarr[-1]
とはかけないので、arr[arr.length-1]
と書く必要があります。これをarr.at(-1)
と書けるようになります。
Top-level await
async / awaitを定義できるのは関数単位でしたが、モジュール単位で非同期関数として動作させることができるようになります。
// awaiting.mjs import { process } from "./some-module.mjs"; const dynamic = import(computedModuleSpecifier); const data = fetch(url); export const output = process((await dynamic).default, await data);
// usage.mjs import { output } from "./awaiting.mjs"; export function outputPlusValue(value) { return output + value } console.log(outputPlusValue(100)); setTimeout(() => console.log(outputPlusValue(100), 1000);
usage.mjs
ではawaiting.mjs
を読み込み、その中のPromiseが解決するまで待ちます。モジュールのロード(import)のタイミングによって結果が変わる(dynamic
やdata
の値がundefiend
のままである)のを防ぐことがでいます。
クラスにプライベートなインスタンスフィールド、アクセッサ
クラスにプライベートなメソッドとアクセッサ(getter、setter)が追加されます。メソッド名に#
をつけるとプライベートになります。
class Counter extends HTMLElement { #xValue = 0; get #x() { return #xValue; } set #x(value) { this.#xValue = value; window.requestAnimationFrame(this.#render.bind(this)); } #clicked() { this.#x++; } constructor() { super(); this.onclick = this.#clicked.bind(this); } connectedCallback() { this.#render(); } #render() { this.textContent = this.#x.toString(); } } window.customElements.define('num-counter', Counter);
クラスに静的なフィールドとメソッド
クラスに静的なフィールドとメソッドが追加できるようになります。
class CustomDate { // ... static epoch = new CustomDate(0); }
クラスに静的イニシャライザブロック
クラスに静的なフィールドとメソッドが定義できるようになりましたが、静的フィールドの初期値に何らかの評価が必要になる場合は、初期値が設定できません。静的なブロックを定義することで、静的フィールドに評価が必要な場合でも、ブロック内で評価できるようになります。
// without static blocks: class C { static x = ...; static y; static z; } try { const obj = doSomethingWith(C.x); C.y = obj.y C.z = obj.z; } catch { C.y = ...; C.z = ...; } // with static blocks: class C { static x = ...; static y; static z; static { try { const obj = doSomethingWith(this.x); this.y = obj.y; this.z = obj.z; } catch { this.y = ...; this.z = ...; } } }
Object.prototype.hasOwn
hasOwnProperty
をもっと使いやすくするために、hasOwn
というのを導入します。
let hasOwnProperty = Object.prototype.hasOwnProperty if (hasOwnProperty.call(object, "foo")) { console.log("has property foo") }
これは以下のように書けるようになります。
if (Object.hasOwn(object, "foo")) { console.log("has property foo") }
Error Cause
例外はcatch
で補足することができますが、複数箇所で例外が発生しうる場合はどこで例外が発生したかを特定するための処理を書くのが面倒です。Error()
のコンストラークターにcause
プロパティを渡すことで、どこで例外が発生したかを特定できるようになります。
async function doJob() { const rawResource = await fetch('//domain/resource-a') .catch(err => { throw new Error('Download raw resource failed', { cause: err }); }); const jobResult = doComputationalHeavyJob(rawResource); await fetch('//domain/upload', { method: 'POST', body: jobResult }) .catch(err => { throw new Error('Upload job result failed', { cause: err }); }); } try { await doJob(); } catch (e) { console.log(e); console.log('Caused by', e.cause); } // Error: Upload job result failed // Caused by TypeError: Failed to fetch
RegExp MatchにIndices
追加
正規表現の結果にindices
プロパティが追加されます。部分文字列の入力文字列に対する位置が簡単にとれるようになります。パフォーマンス上の理由からd
フラグがついた場合のみ有効です。
const re1 = /a+(?<Z>z)?/d; // indicesは入力文字列の先頭からの位置 const s1 = "xaaaz"; const m1 = re1.exec(s1); m1.indices[0][0] === 1; // 部分文字列1つ目の開始位置 m1.indices[0][1] === 5; // 部分文字列1つ目の終了位置 s1.slice(...m1.indices[0]) === "aaaz"; m1.indices[1][0] === 4; // 部分文字列2つ目の開始位置 m1.indices[1][1] === 5; // 部分文字列2つ目の開始位置 s1.slice(...m1.indices[1]) === "z"; m1.indices.groups["Z"][0] === 4; m1.indices.groups["Z"][1] === 5; s1.slice(...m1.indices.groups["Z"]) === "z"; // capture groups that are not matched return `undefined`: const m2 = re1.exec("xaaay"); m2.indices[1] === undefined; m2.indices.groups["Z"] === undefined;
プライベートフィールドの存在チェックをより簡単に
オブジェクトに存在しないプライベートフィールドにアクセスしようとすると例外が発生します。この例外をtry
/catch
で補足しプライベートフィールドが存在するかどうかをチェックすることができますが、in
を使ってもっと簡単にできるようになります。
class C { #brand; #method() {} get #getter() {} static isC(obj) { return #brand in obj && #method in obj && #getter in obj; } }```
下のように書いても同じですがおさまりが悪いので、この仕様が取り入れられました。
class C { #brand; #method() {} get #getter() {} static isC(obj) { try { obj.#brand; obj.#method; obj.#getter; return true; } catch { return false; } } }
ES2023(確定分)
現在ステージ4(Finished)にあるものはES2023に盛り込まれます。
Array find from last
ArrayにfindLast()
とfindLastIndex()
を追加。
const array = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }]; array.find(n => n.value % 2 === 1); // { value: 1 } array.findIndex(n => n.value % 2 === 1); // 0 // ======== Before the proposal =========== // find [...array].reverse().find(n => n.value % 2 === 1); // { value: 3 } // findIndex array.length - 1 - [...array].reverse().findIndex(n => n.value % 2 === 1); // 2 array.length - 1 - [...array].reverse().findIndex(n => n.value === 42); // should be -1, but 4 // ======== In the proposal =========== // find array.findLast(n => n.value % 2 === 1); // { value: 3 } // findIndex array.findLastIndex(n => n.value % 2 === 1); // 2 array.findLastIndex(n => n.value === 42); // -1
Hashbang Grammar
シバン / ハッシュバンの統一した用法。
#!/usr/bin/env node // in the Script Goal 'use strict'; console.log(1);
#!/usr/bin/env node // in the Module Goal export {}; console.log(1);
現在のステージ3(2022年8月15日現在)
現在ステージ3にあがっているもののリストです。この中からES2023に含めるものがあるかもしれませんし、ES2024以降になるものもあります。
Legacy RegExp features in JavaScript
JavaScriptのレガシー(非推奨)なRegExpの機能で互換性の維持のために残したいものです 。
例:RegExp.$1
RegExp.prototype.compile
Atomics.waitAsync
Workerのような異なるエージェント間で共有メモリ(Atomics.notify
で使われる共有メモリ)を通して実行の待ち合わせをすることができるようになります。
var sab = new SharedArrayBuffer(4096); var ia = new Int32Array(sab); ia[37] = 0x1337; test1(); function test1() { Atomics.waitAsync(ia, 37, 0x1337, 1000).then(function (r) { log("Resolved: " + r); test2(); }); } var code = ` var ia = null; onmessage = function (ev) { if (!ia) { postMessage("Aux worker is running"); ia = new Int32Array(ev.data); } postMessage("Aux worker is sleeping for a little bit"); setTimeout(function () { postMessage("Aux worker is waking"); Atomics.notify(ia, 37); }, 1000); }`; function test2() { var w = new Worker("data:application/javascript," + encodeURIComponent(code)); w.onmessage = function (ev) { log(ev.data) }; w.postMessage(sab); Atomics.waitAsync(ia, 37, 0x1337).then(function (r) { log("Resolved: " + r); test3(w); }); } function test3(w) { w.postMessage(false); Atomics.waitAsync(ia, 37, 0x1337).then(function (r) { log("Resolved 1: " + r); }); Atomics.waitAsync(ia, 37, 0x1337).then(function (r) { log("Resolved 2: " + r); }); Atomics.waitAsync(ia, 37, 0x1337).then(function (r) { log("Resolved 3: " + r); }); } function log(msg) { document.getElementById("scrool").innerHTML += String(msg) + "\n"; }
Import Assertions
import文に情報を付加することができるようになります。
import json from "./foo.json" assert { type: "json" }; import("foo.json", { assert: { type: "json" } });
JSON modules
上記のImport Assertions
でJSONモジュールをインポートできるようにする。
Temporal
Date
は使いづらいので、モダンなdate/time APIとしてTemporal
を導入します。
/** * Get the current date in JavaScript * This is a popular question on Stack Overflow for dates in JS * https://stackoverflow.com/questions/1531093/how-do-i-get-the-current-date-in-javascript * */ const date = Temporal.Now.plainDateISO(); // Gets the current date date.toString(); // returns the date in ISO 8601 date format // If you additionally want the time: Temporal.Now.plainDateTimeISO().toString(); // date and time in ISO 8601 format
Resizable and growable ArrayBuffers
ArrayBuffer
を拡張して、バッファサイズのリサイズができるようになります。constructor
で最大値を指定することになります。
let rab = new ArrayBuffer(1024, { maximumByteLength: 1024 ** 2 }); assert(rab.byteLength === 1024); assert(rab.maximumByteLength === 1024 ** 2); assert(rab.resizable); rab.resize(rab.byteLength * 2); assert(rab.byteLength === 1024 * 2); // Transfer the first 1024 bytes. let ab = rab.transfer(1024); // rab is now detached assert(rab.byteLength === 0); assert(rab.maximumByteLength === 0); // The contents are moved to ab. assert(!ab.resizable); assert(ab.byteLength === 1024);
ShadowRealms
異なるJavaScriptプログラムを仮想環境を立ち上げてそこで独立して動かすための仕組みです。各プログラム(が実行されるレルム)でグローバル変数は独立です。
// API(Typescript format) declare class ShadowRealm { constructor(); importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>; evaluate(sourceText: string): PrimitiveValueOrCallable; }
const red = new ShadowRealm(); // realms can import modules that will execute within it's own environment. // When the module is resolved, it captured the binding value, or creates a new // wrapped function that is connected to the callable binding. const redAdd = await red.importValue('./inside-code.js', 'add'); // redAdd is a wrapped function exotic object that chains it's call to the // respective imported binding. let result = redAdd(2, 3); console.assert(result === 5); // yields true // The evaluate method can provide quick code evaluation within the constructed // shadowRealm without requiring any module loading, while it still requires CSP // relaxing. globalThis.someValue = 1; red.evaluate('globalThis.someValue = 2'); // Affects only the ShadowRealm's global console.assert(globalThis.someValue === 1); // The wrapped functions can also wrap other functions the other way around. const setUniqueValue = await red.importValue('./inside-code.js', 'setUniqueValue'); /* setUniqueValue = (cb) => (cb(globalThis.someValue) * 2); */ result = setUniqueValue((x) => x ** 3); console.assert(result === 16); // yields true
Array Grouping
SQLのGROUP BY
のようなグルーピングができるようになります。
const array = [1, 2, 3, 4, 5]; // group groups items by arbitrary key. // In this case, we're grouping by even/odd keys array.group((num, index, array) => { return num % 2 === 0 ? 'even': 'odd'; }); // => { odd: [1, 3, 5], even: [2, 4] } // groupToMap returns items in a Map, and is useful for grouping using // an object key. const odd = { odd: true }; const even = { even: true }; array.groupToMap((num, index, array) => { return num % 2 === 0 ? even: odd; }); // => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }
Decorators
デコ−レーターと呼ばれる関数が使用できるようになります。
下記の例のように関数に@をつけて
クラスやクラスのメソッドやフィールドの前に置き、クラスの定義時、メソッドの定義時、フィールドの初期化時に実行されます。
function logged(value, { kind, name }) { if (kind === "method") { return function (...args) { console.log(`starting ${name} with arguments ${args.join(", ")}`); const ret = value.call(this, ...args); console.log(`ending ${name}`); return ret; }; } } class C { @logged m(arg) {} } new C().m(1); // starting m with arguments 1 // ending m
function logged(value, { kind, name }) { if (kind === "field") { return function (initialValue) { console.log(`initializing ${name} with value ${initialValue}`); return initialValue; }; } // ... } class C { @logged x = 1; } new C(); // initializing x with value 1
RegExp v
flag with set notation + properties of strings
正規表現の差集合、積集合、ネストされた文字集合を使えるようになります。Java/Perl/Python等の言語ではすでにサポートされているものです。
// difference/subtraction [A--B] // intersection [A&&B] // nested character class [A--[0-9]]
以下のように使えます。
// ASCII数字でないものを見つけて、ASCII数字に変換 [\p{Decimal_Number}--[0-9]] // 特定の言語(クメール文字)の文字や識別子を見つける [\p{Script=Khmer}&&[\p{Letter}\p{Mark}\p{Number}]] // ASCII意外の絵文字を見つける [\p{Emoji}--[#*0-9]] [\p{Emoji}--\p{ASCII}]
Change Array by Copy
Arrayにメソッドを追加します。
- Array.prototype.toReversed() -> Array
- Array.prototype.toSorted(compareFn) -> Array
- Array.prototype.toSpliced(start, deleteCount, ...items) -> Array
- Array.prototype.with(index, value) -> Array
const sequence = [1, 2, 3]; sequence.toReversed(); // => [3, 2, 1] sequence; // => [1, 2, 3] const outOfOrder = new Uint8Array([3, 1, 2]); outOfOrder.toSorted(); // => Uint8Array [1, 2, 3] outOfOrder; // => Uint8Array [3, 1, 2] const correctionNeeded = [1, 1, 3]; correctionNeeded.with(1, 2); // => [1, 2, 3] correctionNeeded; // => [1, 1, 3]
Symbols as WeakMap keys
WeakMapのキーとしてSymbolを使用できるようになります。
const weak = new WeakMap(); // Pun not intended: being a symbol makes it become a more symbolic key const key = Symbol('my ref'); const someObject = { /* data data data */ }; weak.set(key, someObject);
JSON.parse source text access
ECMAScriptの値とJSON文字列では情報が失われることがあります。例えば"999999999999999999"
と"999999999999999999.0"
と"1000000000000000000"
をデシリアライズすると全て1000000000000000000
になります。
JSON.parseのreviver関数の引数にソーステキストを渡せるように改良します。
const tooBigForNumber = BigInt(Number.MAX_SAFE_INTEGER) + 2n; const intToBigInt = (key, val, {source}) => typeof val === "number" && val % 1 === 0 ? BigInt(source) : val; const roundTripped = JSON.parse(String(tooBigForNumber), intToBigInt); tooBigForNumber === roundTripped; // → true const bigIntToRawJSON = (key, val) => typeof val === "bigint" ? JSON.rawJSON(val) : val; const embedded = JSON.stringify({ tooBigForNumber }, bigIntToRawJSON); embedded === '{"tooBigForNumber":9007199254740993}'; // → true
Duplicate named capturing groups
str.match(/(?<year>[0-9]{4})-[0-9]{2}|[0-9]{2}-(?<year>[0-9]{4})/)
この正規表現はyear
というグループ名を2度使用しているのでエラーになります。しかし|
で複数の異なる正規表現にマッチさせたいことはあります。このユースケースに対応できるようになります。
まとめ
- 2022年リリースの仕様(ES2022)には
.at()
Top-level await
クラスにプライベートなインスタンスフィールド、アクセッサ
クラスに静的なフィールドとメソッド
クラスに静的イニシャライザブロック
Object.prototype.hasOwn
Error Cause
RegExp MatchにIndices追加
プライベートフィールドの存在チェックをより簡単に
の9つが盛り込まれました。 - ES2023には
Array find from last
Hashbang Grammar
の2つがステージ4にあり、それらが盛り込まれることが決まっています。 - その他にも14の提案がステージ3にあります。