@mizumotokのブログ

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

JavaScript / ES2021の新機能とES2022で予定される機能

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

f:id:mizumotok:20210818180020j:plain

ECMAScriptとは

ECMAScript(エクマスクリプトと読む)とはJavaScriptの標準規格です。JavaScriptはかつてはInternet ExplorerNetscapeのようなブラウザのベンダーが勝手に実装していて、ブラウザごとに言語仕様が異なっていました。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)のタイミングによって結果が変わる(dynamicdataの値が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 AssertionsJSONモジュールをインポートできるようにする。

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

まとめ

  • ECMAScriptJavaScriptの標準規格で、毎年改定されています。
  • 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にあります。