@mizumotokのブログ

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

JavaScript ES2022 / ES2023(予定される機能)

ECMAScriptJavaScriptの標準規格で、2015年以降毎年改定されています。この記事を読むとES2022に採用された仕様、ES2023に決まっている仕様、次に盛り込まれそうな仕様がわかります。

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 公開前

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)のタイミングによって結果が変わる(dynamicdataの値が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 AssertionsJSONモジュールをインポートできるようにする。

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

SQLGROUP 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にあります。