JavaScriptのモダンな書き方 - ES2020のオプショナルチェーン、null合体演算子、動的import、globalThis等を解説

631
521

JavaScriptの仕様であるECMAScriptは年次で仕様が更新されています。ECMAScript 2020(ES2020)は2020年6月にリリースとなりました。現行のすべてのブラウザでES2020の機能は利用できますが、フロントエンドエンジニアにとって使いこなしたい記法ばかりです。

本記事ではES2020に焦点をあて、JavaScriptの新しい記述方法のメリットと使いどころを解説します。

オプショナルチェーン

Optional Chaining(オプショナルチェーンやオプショナルチェーニングと呼ばれています)とは、?.構文を用いてnullundefinedになりうる値へ安全にアクセスできる仕組みです。

利用シーン

  • nullundefinedになるかもしれない値にアクセスしたいとき

何がメリットなのか?

APIからユーザーの住所を取得して出力するプログラムを考えてみましょう。サーバーからは次のようなオブジェクトが返ってくる想定です。

▼ APIから返ってくる想定のオブジェクト

const userData = {
  name: "鈴木",
  address: {
    // データによっては存在しないかもしれない
    city: "港区"
  }
};

上記のオブジェクトからcityを出力したいのですが、ユーザーデータによってはaddressが存在しないかもしれません。従来は、addressの存在を考慮に入れて次のようなコードを記述する必要がありました。

▼ 従来のコード

const address = userData.address;
if(address != null){
  const city = address.city;
  console.log(city);
}
// 結果: addressがある場合は「港区」が出力される

もしくは&&を使って以下のようにも処理を記述していました。

const city = userData.address && userData.address.city;
console.log(city);
// 結果: addressがある場合は「港区」が出力される

オプショナルチェーンを使うと、次のようにコードが記述できます。?がオプショナルチェーン用の演算子です。

▼ オプショナルチェーンを使った新コード

const city = userData.address?.city;
console.log(city);
// 結果: addressがある場合は「港区」が出力される。
//       addressがない場合はundefinedになる(エラーにならない)。

▼ オプショナルチェーンの実行結果

Optional Chainingの実行結果

?.構文は何個でも使用できるので、userDatanullになりえるのであれば次のように記述できます。

?.構文を複数使う例

const city = userData?.address?.city;
// 結果: userDataもaddressもある場合は「港区」が出力される。
//      それ以外はundefinedになる(エラーにならない)。

DOM要素へquerySelector()を用いてアクセスする場合にも便利に使えます。

▼ HTMLコード

<p>こんにちは</p>

pタグ内のテキストを出力する例

const paragraphText = document.querySelector("p")?.innerHTML;
console.log(paragraphText);
// 結果: p要素がある場合は、p要素内のテキストを参照して「こんにちは」が出力される。
//      それ以外はundefinedになる(エラーにならない)。

nullundefinedのときだけ値を返せるnull合体演算子

Nullish coalescing Operator(null合体演算子)は、「A ?? B」という形で用い、AnullundefinedのときだけBを返します。「coalescing」は聞き慣れない単語かもしれませんが、「合体」という意味を持ちます。

構文 意味
A ?? B AnullundefinedのときB

利用シーン

  • 0falseなどにfalseに評価されうる値を正しく扱いたい時

何がメリットなのか?

次のようなオブジェクトから、foo値を取得して出力する関数を考えてみましょう。もしfooが未設定(nullまたはundefined)ならば、「値なし」という文字列を出力したいです。

▼ 対象のオブジェクト

const myObject1 = {
  foo: 1
};

const myObject2 = {
  foo: 0
};

const myObject3 = {
  foo: false
};

const myObject4 = {
  foo: null
};

||(論理OR)演算子(参考:「論理演算子 - JavaScript | MDN」を使って、次のようなgetFoo関数を定義するのは望ましくありませんmyObject2myObject3が引数として渡された場合にobject.foofalseと評価されて「値なし」が出力されるからです。

fooの取得関数のNG例

function getFoo(object) {
  return object.foo || "値なし";
}

console.log(getFoo(myObject1)); // 結果: 1
console.log(getFoo(myObject2)); // 結果: ☆「値なし」(0が期待値)
console.log(getFoo(myObject3)); // 結果: ☆「値なし」(falseが期待値)
console.log(getFoo(myObject4)); // 結果: 「値なし」

ES2020のnull合体演算子を使えば、object.foonullundefinedの場合のみ「値なし」を返すので、目的の挙動となります。

▼ null合体演算子を使った処理

function getFoo(object) {
  return object.foo ?? "値なし";
}

console.log(getFoo(myObject1)); // 結果: 1
console.log(getFoo(myObject2)); // 結果: 0
console.log(getFoo(myObject3)); // 結果: false
console.log(getFoo(myObject4)); // 結果: 「値なし」

▼ null合体演算子の実行結果

Nullish coalescing Operatorの実行結果

関数形式でモジュールを読み込むimport()

import()関数は、関数形式でモジュールを読み込むための仕組みです。dynamic importとも呼ばれます。

従来のimportとは異なり、JavaScriptコードの任意の場所で実行でき、任意のタイミングでモジュールを読み込めます。

メソッド 意味 戻り値
import(モジュール名) モジュールを読み込む Promise

利用シーン

  • モジュールを遅延読み込みしたいとき
  • 関数形式でモジュールを読み込みたい

何がメリットなのか?

次のモジュール「sub.js」を用いて解説します。

▼ モジュールsub.js

export const foo = "Hello World";

従来のimport文はJavaScriptのトップレベルでしか用いることができず、モジュールは即座に読み込まれる仕様でした。

▼ 従来のimport文でモジュールを読み込む処理

// 即座にsub.jsが読み込まれる
import { sub } from "./sub.js";

console.log(sub); // 結果:"Hello World"

ES2020で導入されるimport()関数は、モジュールを動的に読み込みます。巨大なモジュールを遅延して読み込む場合などに便利です。戻り値のPromiseの結果としてモジュールを受け取るため、次のようにすればsub.js内のsub定数にアクセスできます。

import()関数でモジュールを読み込む処理

// import()のタイミングでsub.jsが読み込まれる
import("./sub.js").then(module => {
  // subモジュールの定数を読み込んで出力します
  console.log(module.sub);
});

オブジェクトの分割代入(destructuring assignment)を使うのがよいでしょう。

import()とオブジェクトの分割代入を使用したモジュール読み込み

// import()のタイミングでsub.jsが読み込まれる
import("./sub.js").then(({ sub }) => {
  // subモジュールの定数を読み込んで出力します
  console.log(sub);
});

import()関数は任意のタイミングで実行できるので、次のように画面表示から2秒後にモジュールを読み込むということも可能です。また、ES2017で導入されたasync関数とawait式を使うとよりスッキリコードを記述できます。

▼ 画面表示から2秒後にモジュールを読み込んで定数を出力する例

setTimeout(async () => {
  const { sub } = await import("./sub.js");
  console.log(sub);
}, 2000);

sub.jsモジュールが2秒後に読み込まれている様子

モジュールが2秒後に読み込まれている様子

Promiseの複数処理に便利なPromise.allSettled()

Promise.allSettled()は、複数のPromiseの処理を扱うためのメソッドです。複数のPromiseの1つがrejectされても処理を続行できます。

メソッド 意味 戻り値
Promise.allSettled([Promise1, Promise2, ...]) 各Promiseを実行する Promiseの結果のイテレータ

利用シーン

  • 複数のPromiseを、成功・失敗によらずすべて実行したいとき

何がメリットなのか?

Promiseの複数同時処理といえば、ES2015で導入されたPromise.all()というメソッドがあります。Promise.all()は複数のPromiseの処理を実行し、全部が成功した場合にはじめて処理が終了するものです。もし、1つでもrejectされるものがあればその時点で処理を終了します。

Promise.all()rejectされるものがあった場合

const promiseList = [
  Promise.resolve("😊"),
  Promise.resolve("😊"),
  Promise.reject("😡"),
  Promise.resolve("😊")
];

Promise.all(promiseList).then(
  resolve => console.log(`resolve: ${resolve}`),
  reject => console.log(`reject: ${reject}`)
);

// 結果:「reject: 😡」が出力される

Promise.all()の実行結果

Promise.all()の実行結果

ES2020のPromise.allSettled()では、rejectされるものがあってもすべての処理が実行されます。したがって、複数の配列のどれかが失敗したとしても処理をすべて実行したい場合に便利です。

Promise.allSettled()rejectされるものがあった場合

const promiseList = [
  Promise.resolve("😊"),
  Promise.resolve("😊"),
  Promise.reject("😡"),
  Promise.resolve("😊")
];

Promise.allSettled(promiseList).then(
  resolveList => {
    console.log("resolve");
    for (const resolve of resolveList) {
      console.log(resolve);
    }
  },
  reject => {
    console.log("reject");
    console.log(reject);
  }
);

// 結果:「resolve」のあと、各promiseの値が出力される

Promise.allSettled()の実行結果

Promise.allSettled()の実行結果

関連資料:「Promise.allSettled - tc39

正規表現で一致したものをキャプチャグループとともに返すmatchAll()メソッド

対象文字列について、正規表現で一致したものをキャプチャグループ(参考:「グループと後方参照 - JavaScript | MDN」)とともにイテレータで返すためのメソッドです。一致した部分の各インデックス、各キャプチャーグループを返すことが可能です。

メソッド 意味 戻り値
対象の文字列.matchAll(正規表現) 正規表現で一致したものを返す 文字列のイテレータ

利用シーン

  • 正規表現で一致する複数の文字列のインデックスなどを取り扱いたいとき

何がメリットなのか?

従来は、正規表現で一致した文字列のインデックスやキャプチャグループを取得するには、正規表現.exec()を使う必要がありました。

▼ 従来の処理

const myString = "JavaScriptを覚えよう";
const myRegex = /a/g;
let match;
while ((match = myRegex.exec(myString))) {
  console.log(match);
}

// 結果
// ["a", index: 1, input: "JavaScriptを覚えよう", groups: undefined]
// ["a", index: 3, input: "JavaScriptを覚えよう", groups: undefined]

文字列.matchAll()を使えば、イテレータ形式で処理ができて簡潔に記述可能です。

文字列.matchAll()を使った処理

const myString = "JavaScriptを覚えよう";
const myRegex = /a/g;
for (const match of myString.matchAll(myRegex)) {
  console.log(match);
}

// 結果
// ["a", index: 1, input: "JavaScriptを覚えよう", groups: undefined]
// ["a", index: 3, input: "JavaScriptを覚えよう", groups: undefined]

matchAll()の実行結果

matchAll()の実行結果

ウェブブラウザでもNode.jsもグローバルオブジェクトを参照できるglobalThis

globalThisは、ウェブブラウザ(クライアントサイド)でもNode.js(サーバーサイド)もグローバルオブジェクトを参照できるオブジェクトです。

構文 意味
globalThis グローバルオブジェクトを参照する

利用シーン

  • ウェブブラウザの処理とNode.jsの処理で共通のコードを使いたいとき

何がメリットなのか?

JavaScriptは、ウェブブラウザ上だけではなくNode.jsでも実行されます。しかし、グローバルオブジェクトの参照を考えた場合、ウェブブラウザではwindowがグローバルオブジェクト、Node.jsではglobalがグローバルオブジェクトなので、コードを共通化できませんでした。

globalThisを使えば、ウェブブラウザ・Node.jsに共通でグローバルオブジェクトを参照できるので、コードの共通化に役立ちます。もちろん、従来どおりウェブブラウザでのwindow、Node.jsでのglobalも使用可能です。

次のコードを用いてウェブブラウザでの実行結果を確認してみましょう。

▼ index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <script>
      // globalThisの値を出力します
      console.log(globalThis);
    </script>
  </head>
  <body></body>
</html>

HTMLファイルをChromeで開くと、コンソールタブに次のように出力されます。globalThisによって、Windowが出力されていることがわかります。

▼ ウェブブラウザでglobalThisを参照している様子

ウェブブラウザでglobalThisを参照している様子

※ ブラウザ:Google Chrome v80

続いて、Node.jsでの実行結果を見てみましょう。次のコードが記述されたJavaScriptファイル「script.js」を準備します。

▼ script.js

console.log(globalThis);

コマンドラインでnode script.jsと入力するとNode.jsを実行できます。次のキャプチャーが実行結果です。赤枠部分が実行結果ですが、globalThisによってGlobalが出力されていることがわかります。

▼ Node.jsでglobalThisを参照している様子

Node.jsでglobalThisを参照している様子

※ Node.js v13.7.0

2^53以上の整数を扱えるBigInt

BigIntは、Numberより大きな整数2^53以上の整数を扱えるオブジェクトです。

メソッド 意味 戻り値
BigInt(数値) 数値をBigIntの値にする BigIntの整数
構文 意味
数値n 数値をBigIntの値にする

利用シーン

  • 2^53 以上の整数の計算をしたいとき
  • 符号付きまたは符号なしの64ビット整数を扱いたいとき

何がメリットなのか?

JavaScriptの数値(Number)で正確に扱える最大整数は、2^53-1です。定数でいえば、Number.MAX_SAFE_INTEGERとなります。2^53以上の値を計算しようとすると、計算結果に誤差が生じます。

▼ 計算結果に誤差が生じる例

console.log(Number.MAX_SAFE_INTEGER); // 結果: 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 2); // 結果: 9007199254740992(9007199254740993ではない)

BitIntを使うと、2^53以上の整数も扱えるようになります。BigIntを扱うには、BigInt(数値)とするか、数値nという記述をします。BigIntBigInt同士でしか計算ができないので、Numberとあわせて使う場合はNumberBigIntに変換するようにしましょう。

BigIntの計算例

console.log(BigInt(Number.MAX_SAFE_INTEGER) + 2n);
// 結果: 9007199254740993n

▼ 実行結果

BigIntの実行結果

出力された9007199254740993nは数学世界での整数9007199254740993を指します。

for...inの順序固定

ここでいうfor...inとは、次のように列挙可能なプロパティを反復する処理のことです。

const myData = { name: "鈴木", age: 23, address: "TOKYO" };

for (const key in myData) {
  console.log(`${key}: ${myData[key]}`);
}

従来のECMAScriptの仕様では、for...inの実行順序は固定されていませんでした。ES2020では、その実行順序が指定されます。仕様は次のとおりです。

対応環境の状況

ウェブブラウザの対応状況

本記事で紹介したES2020の仕様は、次のブラウザで対応しています。2023年時点の現行ブラウザですべて対応しています。

機能 Chrome Firefox Safari Edge
オプショナルチェーン ?.構文
null合体演算子 ??演算子
import()
Promise.allSettled()
文字列.matchAll()
globalThis
BigInt
for...inの順序保証

ES2020は便利な新機能がたくさん

今回紹介したES2020の新機能のうち、個人的にはオプショナルチェーン、null合体演算子、import()Promise.allSettled()がとても便利に感じます。新機能はこれまで面倒だった処理を簡潔にしてくれる効果があるので、積極的にキャッチアップしていきましょう。

JavaScriptの新機能の情報は、次の記事にもまとまっています。あわせて参照くださいませ。

※この記事が公開されたのは4年前ですが、1年前の2023年3月に内容をメンテナンスしています。

編集部

ICS MEDIAは株式会社ICSが運営するオウンドメディアです。ICSはインタラクションデザイン専門のプロダクション。最先端のウェブテクノロジーを駆使し、オンスクリーンメディアの表現分野で活動しています。

この担当の記事一覧