ECMAScriptはJavaScriptの標準規格で、2015年以降毎年改定されています。この記事を読むとECMAScriptの仕様策定プロセスが理解でき、ES2021に採用された仕様、ES2022に決まっている仕様、次に盛り込まれそうな仕様がわかります。
ECMAScriptとは
ECMAScript(エクマスクリプトと読む)とはJavaScriptの標準規格です。JavaScriptはかつてはInternet ExplorerやNetscapeのようなブラウザのベンダーが勝手に実装していて、ブラウザごとに言語仕様が異なっていました。JavaScriptが急速に普及していく中、この状況は不便だよねということで、Ecma InternationalがECMA-262という規格番号で標準化しました。
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 | 公開前 |
バージョンの公開時期を見ていると、ES4が放棄されたり、ES5.1からES6まで4年かかったりと、仕様の策定プロセスに混乱があった跡がうかがえます。コミュニティ内の合意がなかなかとれずに苦労したようです。このときの反省からES2015以降は仕様提案の策定プロセスを改め、毎年改定されるようになりました。
ECMAScriptの策定プロセス
ES2015以降では、仕様の提案は5段階のステージに分けられ、リリース時期(毎年6月頃)にステージ4にあるものがリリース対象になります。
ステージ | 状態 | 説明 |
---|---|---|
0 | Strawperson | 仕様の提案 |
1 | Proposal | - 変更を適用したいケースの作成 - 解決方法の説明 - 潜在的な変更の適用 |
2 | Draft | 文法やその意味を言語仕様のフォーマットで記述 |
3 | Candidate | 実装とユーザからのフィードバック待ちの状態 |
4 | Finished | 仕様に追加できる状態 |
ES2021
Finishedのステージにあるものから発行年度が2021年のものがES2021になります。
String.prototype.replaceAll
Stringのreplaceは文字列を置換に使いますが、正規表現を使わないと、最初に当てはまる部分しか置換しません。
const queryString = 'q=query+string+parameters'; const withSpaces = queryString.replace('+', ' '); // => 'q=query string+parameters'
正規表現を使えばできるのですが、+
のような特殊な文字はエスケープしないといけません。
const queryString = 'q=query+string+parameters'; const withSpaces = queryString.replace(/\+/g, ' '); // => 'q=query string parameters'
これが簡単に書けるようになります。
const queryString = 'q=query+string+parameters'; const withSpaces = queryString.replaceAll('+', ' '); // => 'q=query string parameters'
Promise.any
複数のPromiseのうち、どれか一つがresolved
になったら、resolved
になったPromiseを返します。全部のPromiseがrejected
だったら、AggregateError
を返します。
すべての結果を知る必要がなく、一つだけでも成功したかどうかを知りたい場合に使います。
try { const first = await Promise.any(promises); // Any of the promises was fulfilled. } catch (error) { // All of the promises were rejected. }
参考までにその他の関連するPromiseの処理の関係性は以下のようになっています。
関数名 | 説明 | リリース |
---|---|---|
Promise.allSettled | すべてが解決されるかすべてが拒否されるまで待つ | ES2020 |
Promise.all | すべて解決されるか1つが拒否されるまで待つ | ES2015 |
Promise.race | 1つが解決されるか1つが拒否されるまで待つ | ES2015 |
Promise.any | 1つが解決されるかすべてが拒否されるまで待つ | ES2021 |
WeakRefs
オブジェクトへの弱い参照です。弱い参照とはガベージコレクションが実行されたときに、メモリからオブジェクトが消されてundefiend
になります。
通常の参照では、オブジェクトがメモリ上に作られ、そのオブジェクトが変数か何かから参照があったときには、ガベージコレクションが実行されても残ります。ガベージコレクションは参照のないメモリ上のオブジェクトを削除するのです。しかし、弱い参照はガベージコレクションの削除対象になります。
キャッシュのように、値があると使えるけど、なくても作ればいいようなときに使います。
// This technique is incomplete; see below. function makeWeakCached(f) { const cache = new Map(); return key => { const ref = cache.get(key); if (ref) { const cached = ref.deref(); if (cached !== undefined) return cached; } const fresh = f(key); cache.set(key, new WeakRef(fresh)); return fresh; }; } var getImageCached = makeWeakCached(getImage);
Logical Assignment Operators
||=
&&=
??=
が使えるようになりました。
// "Or Or Equals" (or, the Mallet operator :wink:) a ||= b; a || (a = b); // "And And Equals" a &&= b; a && (a = b); // "QQ Equals" a ??= b; a ?? (a = b);
Numeric separators
数値は大きくなると何桁かわかりにくいのですが、_
を数値の間に入れてもよくなりました。
1_000_000_000 // Ah, so a billion(10億) 101_475_938.38 // And this is hundreds of millions(約1億) let fee = 123_00; // $123 (12300 cents, apparently)(123.00ドル=12300セント) let fee = 12_300; // $12,300 (woah, that fee!)(12,300ドル) let amount = 12345_00; // 12,345 (1234500 cents, apparently)(12,345.00ドル=1234500セント) let amount = 123_4500; // 123.45 (4-fixed financial) let amount = 1_234_500; // 1,234,500
ES2022(確定分)
現在ステージ4にあるものはES2022に盛り込まれます。
Class Fields
Private instance methods and accessors
クラスにプライベートなメソッドとアクセッサ(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 Public Instance Fields & Private Instance Fields
クラスにフィールド(パブリック、プライベート)を追加できるようになります。フィールド名に#
をつけるとプライベートになります。(上記の例の#xValue
)
Static class fields and private static methods
クラスに静的なフィールドとメソッドが追加できるようになります。
class CustomDate { // ... static epoch = new CustomDate(0); }
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;
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
のままである)のを防ぐことがでいます。
Ergonomic brand checks for Private Fields
オブジェクトに存在しないプライベートフィールドにアクセスしようとすると例外が発生します。この例外を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; } } }
現在のステージ3(2021年8月18日現在)
現在ステージ3にあがっているもののリストです。この中からES2022に含めるものがあるかもしれませんし、ES2023以降になるものもあります。
Legacy RegExp features in JavaScript
JavaScriptのレガシー(非推奨)なRegExpの機能で互換性の維持のために残したいものです 。
例:RegExp.$1
RegExp.prototype.compile
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);
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"; }
.at()
配列の負の添字を可能にします。
配列の最後をとるのにarr[-1]
とはかけないので、arr[arr.length-1]
と書く必要があります。これをarr.at(-1)
と書けるようになります。
Import Assertions
import文に情報を付加することができるようになります。
import json from "./foo.json" assert { type: "json" }; import("foo.json", { assert: { type: "json" } });
JSON modules
上記のImport Assertions
でJSONモジュールをインポートできるようにする。
Class Static Block
ES2022でクラスに静的なフィールドとメソッドが定義できるようになりましたが、静的フィールドの初期値に何らかの評価が必要になる場合は、初期値が設定できません。静的なブロックを定義することで、静的フィールドに評価が必要な場合でも、ブロック内で評価できるようになります。
// 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 = ...; } } }
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
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
Accessible Object.prototype.hasOwnProperty
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") }
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);
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
Realms
異なるJavaScriptプログラムを仮想環境を立ち上げてそこで独立して動かすための仕組みです。各プログラム(が実行されるレルム)でグローバル変数は独立です。
// API declare class Realm { constructor(); importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>; evaluate(sourceText: string): PrimitiveValueOrCallable; }
const red = new Realm(); // 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 // realm without requiring any module loading, while it still requires CSP // relaxing. globalThis.someValue = 1; red.evaluate('globalThis.someValue = 2'); // Affects only the Realm'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'); /* setUnitValue = (cb) => (cb(globalThis.someValue) * 2); */ result = setUniqueValue((x) => x ** 3); console.assert(result === 16); // yields true
まとめ
- ECMAScriptはJavaScriptの標準規格で、毎年改定されています。
- ECMAScriptにあげられた提案は4つのステージに分類され、ステージ4まであがったものを仕様としてリリースされるプロセスになっています。
- 2021年リリースの仕様(ES2021)には
String.prototype.replaceAll
Promise.any
WeakRefs
Logical Assignment Operators
Numeric separators
の5つが盛り込まれました。 - ES2022には
Class Fields
RegExp Match Indices
Top-level await
Ergonomic brand checks for Private Field
の4つがステージ4にあり、それらが盛り込まれることが決まっています。 - その他にも13の提案がステージ3にあります。