せっかちなプログラマーのためのJavaScript (ES2022版)
この本をサポートしてください: 購入する または 寄付する
(広告です。ブロックしないでください。)

27 モジュール



27.1 チートシート: モジュール

27.1.1 エクスポート

// Named exports
export function f() {}
export const one = 1;
export {foo, b as bar};

// Default exports
export default function f() {} // declaration with optional name
// Replacement for `const` (there must be exactly one value)
export default 123;

// Re-exporting from another module
export {foo, b as bar} from './some-module.mjs';
export * from './some-module.mjs';
export * as ns from './some-module.mjs'; // ES2020

27.1.2 インポート

// Named imports
import {foo, bar as b} from './some-module.mjs';
// Namespace import
import * as someModule from './some-module.mjs';
// Default import
import someModule from './some-module.mjs';

// Combinations:
import someModule, * as someModule from './some-module.mjs';
import someModule, {foo, bar as b} from './some-module.mjs';

// Empty import (for modules with side effects)
import './some-module.mjs';

27.2 JavaScriptのソースコード形式

JavaScriptモジュールの現在の状況は非常に多様です。ES6で組み込みモジュールが導入されましたが、それ以前のソースコード形式もまだ存在しています。後者を理解することは、前者を理解するのに役立つため、調べてみましょう。次のセクションでは、JavaScriptソースコードを配信する以下の方法について説明します。

表. 18は、これらのコード形式の概要を示しています。CommonJSモジュールとECMAScriptモジュールでは、2つのファイル名拡張子が一般的に使用されることに注意してください。どちらが適切かは、ファイルをどのように使用したいかによって異なります。詳細については、この章で後述します。

表18: JavaScriptソースコードを配信する方法。
実行環境 読み込み ファイル名拡張子
スクリプト ブラウザ 非同期 .js
CommonJSモジュール サーバー 同期 .js .cjs
AMDモジュール ブラウザ 非同期 .js
ECMAScriptモジュール ブラウザとサーバー 非同期 .js .mjs

27.2.1 組み込みモジュールが登場する前のコードはECMAScript 5で記述されていた

組み込みモジュール(ES6で導入された)に入る前に、これから見るすべてのコードはES5で記述されます。特に

27.3 モジュールが登場する前はスクリプトがあった

当初、ブラウザにはスクリプトだけがありました。これは、グローバルスコープで実行されるコードの断片です。例として、次のHTMLを介してスクリプトファイルをロードするHTMLファイルを考えてみましょう。

<script src="other-module1.js"></script>
<script src="other-module2.js"></script>
<script src="my-module.js"></script>

メインファイルはmy-module.jsで、モジュールをシミュレートします。

var myModule = (function () { // Open IIFE
  // Imports (via global variables)
  var importedFunc1 = otherModule1.importedFunc1;
  var importedFunc2 = otherModule2.importedFunc2;

  // Body
  function internalFunc() {
    // ···
  }
  function exportedFunc() {
    importedFunc1();
    importedFunc2();
    internalFunc();
  }

  // Exports (assigned to global variable `myModule`)
  return {
    exportedFunc: exportedFunc,
  };
})(); // Close IIFE

myModuleは、関数式を即時呼び出した結果が代入されるグローバル変数です。関数式は1行目で始まり、最終行で呼び出されます。

コードの断片をラップするこの方法は、即時実行関数式(IIFE、ベン・アルマンが命名)と呼ばれます。IIFEから何が得られるのでしょうか?varは(constletのように)ブロックスコープではなく、関数スコープです。つまり、varで宣言された変数の新しいスコープを作成する唯一の方法は、関数またはメソッドを使用することです(constletでは、関数、メソッド、またはブロック{}を使用できます)。したがって、例のIIFEは、次のすべての変数をグローバルスコープから隠し、名前の衝突を最小限に抑えます:importedFunc1importedFunc2internalFuncexportedFunc

特定の形でIIFEを使用していることに注意してください。最後に、エクスポートしたいものを選択し、オブジェクトリテラルを介してそれを返します。これは、リビールモジュールパターン(クリスチャン・ハイルマンが命名)と呼ばれます。

モジュールをシミュレートするこの方法には、いくつかの問題があります。

27.4 ES6より前に作成されたモジュールシステム

ECMAScript 6より前は、JavaScriptには組み込みのモジュールはありませんでした。したがって、言語の柔軟な構文を使用して、言語にカスタムモジュールシステムを実装しました。2つの人気のあるものは次のとおりです。

27.4.1 サーバー側: CommonJSモジュール

モジュールに関するオリジナルのCommonJS標準は、サーバーおよびデスクトッププラットフォーム向けに作成されました。これは、元のNode.jsモジュールシステムの基礎であり、そこで絶大な人気を博しました。その人気に貢献したのは、Nodeのnpmパッケージマネージャーと、クライアント側(browserify、webpackなど)でNodeモジュールを使用できるようにするツールでした。

今後、CommonJSモジュールとは、この標準のNode.jsバージョン(いくつかの追加機能があります)を意味します。これはCommonJSモジュールの例です。

// Imports
var importedFunc1 = require('./other-module1.js').importedFunc1;
var importedFunc2 = require('./other-module2.js').importedFunc2;

// Body
function internalFunc() {
  // ···
}
function exportedFunc() {
  importedFunc1();
  importedFunc2();
  internalFunc();
}

// Exports
module.exports = {
  exportedFunc: exportedFunc,
};

CommonJSは次のように特徴づけられます。

27.4.2 クライアント側: AMD (非同期モジュール定義) モジュール

AMDモジュール形式は、CommonJS形式よりもブラウザで使いやすいように作成されました。最も人気のある実装は、RequireJSです。以下は、AMDモジュールの例です。

define(['./other-module1.js', './other-module2.js'],
  function (otherModule1, otherModule2) {
    var importedFunc1 = otherModule1.importedFunc1;
    var importedFunc2 = otherModule2.importedFunc2;

    function internalFunc() {
      // ···
    }
    function exportedFunc() {
      importedFunc1();
      importedFunc2();
      internalFunc();
    }
    
    return {
      exportedFunc: exportedFunc,
    };
  });

AMDは次のように特徴づけられます。

良い点として、AMDモジュールは直接実行できます。対照的に、CommonJSモジュールは、デプロイ前にコンパイルするか、カスタムソースコードを生成して動的に評価する必要がありますeval()を考えてください)。これはWeb上で常に許可されているとは限りません。

27.4.3 JavaScriptモジュールの特性

CommonJSとAMDを見ると、JavaScriptモジュールシステムの間には類似点があります。

27.5 ECMAScriptモジュール

ECMAScriptモジュール (ESモジュールまたはESM) は、ES6で導入されました。これらは、JavaScriptモジュールの伝統を継承し、前述のすべての特性を備えています。さらに

ESモジュールには新しい利点もあります。

これはESモジュールの構文の例です。

import {importedFunc1} from './other-module1.mjs';
import {importedFunc2} from './other-module2.mjs';

function internalFunc() {
  ···
}

export function exportedFunc() {
  importedFunc1();
  importedFunc2();
  internalFunc();
}

今後、「モジュール」とは「ECMAScriptモジュール」を意味します。

27.5.1 ESモジュール: 構文、意味、ローダーAPI

ESモジュールの完全な標準には、次の部分が含まれます。

  1. 構文(コードの記述方法): モジュールとは何か?インポートとエクスポートはどのように宣言されるか?など。
  2. 意味(コードの実行方法): 変数バインディングはどのようにエクスポートされるか?インポートはエクスポートとどのように接続されるか?など。
  3. モジュールローディングを構成するためのプログラムによるローダーAPI。

パート1と2はES6で導入されました。パート3の作業は進行中です。

27.6 名前付きエクスポートとインポート

27.6.1 名前付きエクスポート

各モジュールには、ゼロ個以上の名前付きエクスポートを含めることができます。

例として、次の2つのファイルを考えてみましょう。

lib/my-math.mjs
main.mjs

モジュールmy-math.mjsには、2つの名前付きエクスポートがあります: squareLIGHTSPEED

// Not exported, private to module
function times(a, b) {
  return a * b;
}
export function square(x) {
  return times(x, x);
}
export const LIGHTSPEED = 299792458;

何かをエクスポートするには、宣言の前にキーワードexportを置きます。エクスポートされないエンティティはモジュールに対してプライベートであり、外部からアクセスすることはできません。

27.6.2 名前付きインポート

モジュールmain.mjsは、squareという名前付きインポートを1つ持っています。

import {square} from './lib/my-math.mjs';
assert.equal(square(3), 9);

インポート名を変更することもできます。

import {square as sq} from './lib/my-math.mjs';
assert.equal(sq(3), 9);
27.6.2.1 構文上の落とし穴: 名前付きインポートは分割代入ではない

名前付きインポートと分割代入は見た目が似ています。

import {foo} from './bar.mjs'; // import
const {foo} = require('./bar.mjs'); // destructuring

しかし、これらはまったく異なります。

  演習: 名前付きエクスポート

exercises/modules/export_named_test.mjs

27.6.3 名前空間インポート

名前空間インポートは、名前付きインポートの代替手段です。モジュールを名前空間インポートすると、そのプロパティが名前付きエクスポートとなるオブジェクトになります。名前空間インポートを使用した場合のmain.mjsは次のようになります。

import * as myMath from './lib/my-math.mjs';
assert.equal(myMath.square(3), 9);

assert.deepEqual(
  Object.keys(myMath), ['LIGHTSPEED', 'square']);

27.6.4 名前付きエクスポートのスタイル: インラインと節 (高度)

これまで見てきた名前付きエクスポートのスタイルはインラインでした。エンティティの前にキーワードexportを付けることでエクスポートしました。

しかし、別のエクスポート節を使用することもできます。たとえば、エクスポート節を使用したlib/my-math.mjsは次のようになります。

function times(a, b) {
  return a * b;
}
function square(x) {
  return times(x, x);
}
const LIGHTSPEED = 299792458;

export { square, LIGHTSPEED }; // semicolon!

エクスポート節を使用すると、エクスポート前に名前を変更したり、内部で異なる名前を使用したりできます。

function times(a, b) {
  return a * b;
}
function sq(x) {
  return times(x, x);
}
const LS = 299792458;

export {
  sq as square,
  LS as LIGHTSPEED, // trailing comma is optional
};

27.7 デフォルトエクスポートとインポート

各モジュールは、最大で1つのデフォルトエクスポートを持つことができます。モジュールがデフォルトエクスポートされた値であるというのがアイデアです。

  名前付きエクスポートとデフォルトエクスポートを混在させない

モジュールは名前付きエクスポートとデフォルトエクスポートの両方を持つことができますが、通常はモジュールごとに1つのエクスポートスタイルに固執する方が良いでしょう。

デフォルトエクスポートの例として、次の2つのファイルを考えてみましょう。

my-func.mjs
main.mjs

モジュールmy-func.mjsはデフォルトエクスポートを持っています。

const GREETING = 'Hello!';
export default function () {
  return GREETING;
}

モジュールmain.mjsはエクスポートされた関数をデフォルトインポートします。

import myFunc from './my-func.mjs';
assert.equal(myFunc(), 'Hello!');

構文上の違いに注意してください。名前付きインポートの周りの波括弧は、モジュールにアクセスしていることを示し、デフォルトインポートはモジュールそのものです。

  デフォルトエクスポートのユースケースは何ですか?

デフォルトエクスポートの最も一般的なユースケースは、単一の関数または単一のクラスを含むモジュールです。

27.7.1 デフォルトエクスポートの2つのスタイル

デフォルトエクスポートを行うには、2つのスタイルがあります。

まず、既存の宣言にexport defaultというラベルを付けることができます。

export default function myFunc() {} // no semicolon!
export default class MyClass {} // no semicolon!

次に、値を直接デフォルトエクスポートできます。このexport defaultのスタイルは、宣言に非常によく似ています。

export default myFunc; // defined elsewhere
export default MyClass; // defined previously
export default Math.sqrt(2); // result of invocation is default-exported
export default 'abc' + 'def';
export default { no: false, yes: true };
27.7.1.1 デフォルトエクスポートのスタイルが2つあるのはなぜですか?

理由は、export defaultを使用してconstにラベルを付けることができないためです。constは複数の値を定義できますが、export defaultは正確に1つの値を必要とします。次の仮想コードを考えてみましょう。

// Not legal JavaScript!
export default const foo = 1, bar = 2, baz = 3;

このコードでは、3つの値のうちどれがデフォルトエクスポートであるかわかりません。

  演習: デフォルトエクスポート

exercises/modules/export_default_test.mjs

27.7.2 名前付きエクスポートとしてのデフォルトエクスポート (高度)

内部的には、デフォルトエクスポートは、名前がdefaultである名前付きエクスポートにすぎません。例として、デフォルトエクスポートを持つ前のモジュールmy-func.mjsを考えてみましょう。

const GREETING = 'Hello!';
export default function () {
  return GREETING;
}

次のモジュールmy-func2.mjsは、そのモジュールと同等です。

const GREETING = 'Hello!';
function greet() {
  return GREETING;
}

export {
  greet as default,
};

インポートの場合、通常のデフォルトインポートを使用できます。

import myFunc from './my-func2.mjs';
assert.equal(myFunc(), 'Hello!');

または、名前付きインポートを使用できます。

import {default as myFunc} from './my-func2.mjs';
assert.equal(myFunc(), 'Hello!');

デフォルトエクスポートは、名前空間インポートのプロパティ.defaultからも利用できます。

import * as mf from './my-func2.mjs';
assert.equal(mf.default(), 'Hello!');

  defaultは変数名として違法ではありませんか?

defaultは変数名にはできませんが、エクスポート名やプロパティ名にはできます。

const obj = {
  default: 123,
};
assert.equal(obj.default, 123);

27.8 エクスポートとインポートの詳細

27.8.1 インポートはエクスポートの読み取り専用ビューである

これまで、インポートとエクスポートを直感的に使用しており、すべてが期待どおりに機能しているように見えます。しかし、今度はインポートとエクスポートが実際どのように関連しているかを詳しく見ていく必要があります。

次の2つのモジュールを考えてみましょう。

counter.mjs
main.mjs

counter.mjsは、(可変の!)変数と関数をエクスポートします。

export let counter = 3;
export function incCounter() {
  counter++;
}

main.mjsは両方のエクスポートを名前付きインポートします。incCounter()を使用すると、counterへの接続がライブであることがわかります。つまり、常にその変数のライブ状態にアクセスできます。

import { counter, incCounter } from './counter.mjs';

// The imported value `counter` is live
assert.equal(counter, 3);
incCounter();
assert.equal(counter, 4);

接続はライブであり、counterを読み取ることができますが、この変数(例: counter++を使用)を変更することはできないことに注意してください。

このようにインポートを処理することには、2つの利点があります。

27.8.2 ESMの循環インポートに対する透過的なサポート (高度)

ESMは循環インポートを透過的にサポートします。それをどのように実現しているかを理解するために、次の例を考えてみましょう。図 7は、他のモジュールをインポートするモジュールの有向グラフを示しています。PがMをインポートすることが、この場合のサイクルです。

Figure 7: A directed graph of modules importing modules: M imports N and O, N imports P and Q, etc.

解析後、これらのモジュールは2つのフェーズで設定されます。

このアプローチは、ESモジュールの2つの機能により、循環インポートを正しく処理します。

27.9 npmパッケージ

npmソフトウェアレジストリは、Node.jsおよびWebブラウザ向けのJavaScriptライブラリおよびアプリを配布する主要な方法です。これは、npmパッケージマネージャー (略してnpm) を介して管理されます。ソフトウェアは、いわゆるパッケージとして配布されます。パッケージは、任意のファイルと、パッケージを記述するトップレベルのファイルpackage.jsonを含むディレクトリです。たとえば、npmがディレクトリmy-package/内に空のパッケージを作成すると、次のpackage.jsonが得られます。

{
  "name": "my-package",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

これらのプロパティの一部には、単純なメタデータが含まれています。

その他のプロパティを使用すると、高度な構成が可能になります。

package.jsonの詳細については、npmドキュメントを参照してください。

27.9.1 パッケージはディレクトリnode_modules/内にインストールされる

npmは常にパッケージをディレクトリnode_modules内にインストールします。通常、これらのディレクトリは多数あります。npmが使用するディレクトリは、現在いるディレクトリによって異なります。たとえば、ディレクトリ/tmp/a/b/内にいる場合、npmは現在のディレクトリ、その親ディレクトリ、親の親ディレクトリなどでnode_modulesを見つけようとします。つまり、次のロケーションのチェーンを検索します。

パッケージsome-pkgをインストールする場合、npmは最も近いnode_modulesを使用します。たとえば、/tmp/a/b/内にいて、そのディレクトリにnode_modulesがある場合、npmはパッケージをディレクトリ内に配置します。

/tmp/a/b/node_modules/some-pkg/

モジュールをインポートする場合、インストールされたパッケージからインポートしたいことをNode.jsに伝えるための特別なモジュール指定子を使用できます。それがどのように機能するかは後で説明します。今のところ、次の例を考えてみましょう。

// /home/jane/proj/main.mjs
import * as theModule from 'the-package/the-module.mjs';

the-module.mjs(Node.jsは、ESモジュールにファイル拡張子.mjsを優先します)を見つけるために、Node.jsはnode_moduleチェーンをたどり、次の場所を検索します。

27.9.2 npmをフロントエンドライブラリのインストールに使用できるのはなぜですか?

node_modulesディレクトリにインストールされたモジュールを見つけることは、Node.jsでのみサポートされています。では、なぜnpmを使用してブラウザ用のライブラリもインストールできるのでしょうか?

これは、オンラインにデプロイする前にコードをコンパイルおよび最適化するwebpackなどのバンドルツールを介して有効になります。このコンパイルプロセス中に、npmパッケージのコードはブラウザで動作するように調整されます。

27.10 モジュールの名前付け

モジュールファイルの名前と、それらがインポートされる変数には、確立されたベストプラクティスはありません。

この章では、次の命名スタイルを使用しています。

このスタイルの背後にある理論的根拠は何ですか?

また、アンダースコアケースのモジュールファイル名も気に入っています。これらの名前を名前空間インポートに(変換なしで)直接使用できるからです。

import * as my_module from './my_module.mjs';

しかし、そのスタイルはデフォルトインポートには適していません。名前空間オブジェクトにはアンダースコアケースが好きですが、関数などには適切な選択ではありません。

27.11 モジュール指定子

モジュール指定子は、モジュールを識別する文字列です。ブラウザとNode.jsでは動作が若干異なります。違いを見る前に、モジュール指定子のさまざまなカテゴリについて学ぶ必要があります。

27.11.1 モジュール指定子のカテゴリ

ESモジュールでは、次のカテゴリの指定子を区別します。これらのカテゴリは、CommonJSモジュールに由来しています。

27.11.2 ブラウザでのESモジュール指定子

ブラウザはモジュール指定子を次のように処理します

バンドルツール(webpackなど、モジュールをより少ないファイルに結合するもの)は、ブラウザよりも指定子に厳密でないことが多いことに注意してください。これらは(実行時ではなく)ビルド/コンパイル時に動作し、ファイルシステムをトラバースしてファイルを検索できるためです。

27.11.3 Node.jsでのESモジュール指定子

Node.jsはモジュール指定子を次のように処理します

ベアパスを除くすべての指定子は、実際のファイルを参照する必要があります。つまり、ESMは次のCommonJS機能をサポートしていません

すべての組み込みNode.jsモジュールは、ベアパスを介して使用でき、名前付きのESMエクスポートがあります。たとえば

import * as assert from 'assert/strict';
import * as path from 'path';

assert.equal(
  path.join('a/b/c', '../d'), 'a/b/d');
27.11.3.1 Node.jsでのファイル名拡張子

Node.jsは、次のデフォルトのファイル名拡張子をサポートしています

ファイル名拡張子.jsは、ESMまたはCommonJSのいずれかを表します。どちらであるかは、「最も近い」package.json(現在のディレクトリ、親ディレクトリなど)を介して構成されます。この方法でのpackage.jsonの使用は、パッケージとは独立しています。

そのpackage.jsonには、2つの設定を持つプロパティ"type"があります

27.11.3.2 ファイル以外のソースコードをCommonJSまたはESMとして解釈する

Node.jsによって実行されるすべてのソースコードがファイルから来ているわけではありません。stdin、--eval、および--printを介してコードを送信することもできます。コマンドラインオプション--input-typeを使用すると、そのようなコードをどのように解釈するかを指定できます。

27.12 import.meta – 現在のモジュールのメタデータ [ES2020]

オブジェクトimport.metaは、現在のモジュールのメタデータを保持します。

27.12.1 import.meta.url

import.metaの最も重要なプロパティは、現在のモジュールのファイルのURLを含む文字列である.urlです。たとえば

'https://example.com/code/main.mjs'

27.12.2 import.meta.urlとクラスURL

クラスURLは、ブラウザーとNode.jsでグローバル変数として使用できます。Node.jsドキュメントでそのすべての機能を確認できます。import.meta.urlを使用する場合、そのコンストラクターは特に役立ちます

new URL(input: string, base?: string|URL)

パラメーターinputには、解析されるURLが含まれています。2番目のパラメーターbaseが提供されている場合、相対パスにすることができます。

言い換えれば、このコンストラクターを使用すると、ベースURLに対して相対パスを解決できます。

> new URL('other.mjs', 'https://example.com/code/main.mjs').href
'https://example.com/code/other.mjs'
> new URL('../other.mjs', 'https://example.com/code/main.mjs').href
'https://example.com/other.mjs'

これが、現在のモジュールの隣にあるファイルdata.txtを指すURLインスタンスを取得する方法です

const urlOfData = new URL('data.txt', import.meta.url);

27.12.3 Node.jsでのimport.meta.url

Node.jsでは、import.meta.urlは常にfile: URLを含む文字列です。たとえば

'file:///Users/rauschma/my-module.mjs'
27.12.3.1 例:モジュールの兄弟ファイルの読み取り

多くのNode.jsファイルシステム操作は、パスを含む文字列またはURLのインスタンスを受け入れます。これにより、現在のモジュールの兄弟ファイルdata.txtを読み取ることができます

import * as fs from 'fs';
function readData() {
  // data.txt sits next to current module
  const urlOfData = new URL('data.txt', import.meta.url);
  return fs.readFileSync(urlOfData, {encoding: 'UTF-8'});
}
27.12.3.2 モジュールfsとURL

モジュールfsのほとんどの関数では、次の方法でファイルを参照できます

このトピックの詳細については、Node.js APIドキュメントを参照してください。

27.12.3.3 file: URLとパス間の変換

Node.jsモジュールurlには、file: URLとパスを変換するための2つの関数があります

ローカルファイルシステムで使用できるパスが必要な場合、URLインスタンスのプロパティ.pathnameは常に機能するとは限りません

assert.equal(
  new URL('file:///tmp/with%20space.txt').pathname,
  '/tmp/with%20space.txt');

したがって、fileURLToPath()を使用することをお勧めします

import * as url from 'url';
assert.equal(
  url.fileURLToPath('file:///tmp/with%20space.txt'),
  '/tmp/with space.txt'); // result on Unix

同様に、pathToFileURL()は、絶対パスに'file://'を追加するだけではありません。

27.13 import()を介したモジュールの動的ロード [ES2020] (高度)

  import()演算子はPromiseを使用します

Promiseは、非同期的に(つまり、すぐにではない)計算される結果を処理するための手法です。これらは、§40「非同期プログラミングのためのPromise [ES6]」で説明されています。それらを理解するまで、このセクションの読み取りを延期するのが理にかなっているかもしれません。

27.13.1 静的importステートメントの制限

これまでのところ、モジュールをインポートする唯一の方法はimportステートメントを使用することでした。そのステートメントにはいくつかの制限があります

27.13.2 import()演算子による動的インポート

import()演算子には、importステートメントの制限はありません。次のようになります

import(moduleSpecifierStr)
.then((namespaceObject) => {
  console.log(namespaceObject.namedExport);
});

この演算子は関数のように使用され、モジュール指定子を含む文字列を受け取り、名前空間オブジェクトに解決されるPromiseを返します。そのオブジェクトのプロパティは、インポートされたモジュールのエクスポートです。

import()は、awaitを介して使用するとさらに便利です

const namespaceObject = await import(moduleSpecifierStr);
console.log(namespaceObject.namedExport);

awaitはモジュールの最上位レベルで使用できることに注意してください(次のセクションを参照)。

import()を使用する例を見てみましょう。

27.13.2.1 例:モジュールの動的ロード

次のファイルを考えてください

lib/my-math.mjs
main1.mjs
main2.mjs

モジュールmy-math.mjsはすでに見てきました

// Not exported, private to module
function times(a, b) {
  return a * b;
}
export function square(x) {
  return times(x, x);
}
export const LIGHTSPEED = 299792458;

import()を使用して、このモジュールをオンデマンドでロードできます

// main1.mjs
const moduleSpecifier = './lib/my-math.mjs';

function mathOnDemand() {
  return import(moduleSpecifier)
  .then(myMath => {
    const result = myMath.LIGHTSPEED;
    assert.equal(result, 299792458);
    return result;
  });
}

mathOnDemand()
.then((result) => {
  assert.equal(result, 299792458);
});

このコードには、importステートメントでは実行できない2つのことがあります

次に、main1.mjsと同じ機能を実装しますが、非同期関数またはasync/awaitと呼ばれる機能を使用します。これにより、Promiseのより優れた構文が提供されます。

// main2.mjs
const moduleSpecifier = './lib/my-math.mjs';

async function mathOnDemand() {
  const myMath = await import(moduleSpecifier);
  const result = myMath.LIGHTSPEED;
  assert.equal(result, 299792458);
  return result;
}

  なぜimport()は関数ではなく演算子なのですか?

import()は関数のように見えますが、関数として実装することはできませんでした

27.13.3 import()のユースケース

27.13.3.1 オンデマンドでのコードのロード

Webアプリの一部の機能は、起動時に存在する必要はなく、必要に応じてロードできます。その際、import() が役立ちます。なぜなら、そのような機能をモジュールに入れることができるからです。例えば、

button.addEventListener('click', event => {
  import('./dialogBox.mjs')
    .then(dialogBox => {
      dialogBox.open();
    })
    .catch(error => {
      /* Error handling */
    })
});
27.13.3.2 モジュールの条件付きロード

条件が真であるかどうかに応じてモジュールをロードしたい場合があります。例えば、ポリフィルを含むモジュールは、レガシープラットフォームで新しい機能を利用可能にするものです。

if (isLegacyPlatform()) {
  import('./my-polyfill.mjs')
    .then(···);
}
27.13.3.3 計算されたモジュール指定子

国際化などのアプリケーションでは、モジュール指定子を動的に計算できると便利です。

import(`messages_${getLocale()}.mjs`)
  .then(···);

27.14 モジュールにおけるトップレベルの await [ES2022] (高度)

  await は非同期関数の機能です

await については、§41「非同期関数」で説明しています。非同期関数を理解するまで、このセクションの読解を後回しにするのも良いでしょう。

モジュールのトップレベルで await 演算子を使用できます。その場合、モジュールは非同期になり、異なる動作をします。ありがたいことに、これは言語によって透過的に処理されるため、プログラマーとして通常はそれを意識することはありません。

27.14.1 トップレベルの await のユースケース

モジュールのトップレベルで await 演算子を使用する理由はなんでしょうか?これにより、非同期でロードされたデータでモジュールを初期化できます。次の3つのサブセクションでは、それが役立つ3つの例を示します。

27.14.1.1 モジュールの動的なロード
const params = new URLSearchParams(location.search);
const language = params.get('lang');
const messages = await import(`./messages-${language}.mjs`); // (A)

console.log(messages.welcome);

A行では、モジュールを動的にインポートしています。トップレベルの await のおかげで、通常の静的なインポートを使用するのとほとんど同じくらい便利です。

27.14.1.2 モジュールのロードに失敗した場合のフォールバックの使用
let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}
27.14.1.3 最も速くロードされるリソースの使用
const resource = await Promise.any([
  fetch('http://example.com/first.txt')
    .then(response => response.text()),
  fetch('http://example.com/second.txt')
    .then(response => response.text()),
]);

Promise.any()により、変数 resource は、最初にダウンロードが完了したリソースで初期化されます。

27.14.2 トップレベルの await はどのように動作するのか?

次の2つのファイルを考えてみましょう。

first.mjs:

const response = await fetch('http://example.com/first.txt');
export const first = await response.text();

main.mjs:

import {first} from './first.mjs';
import {second} from './second.mjs';
assert.equal(first, 'First!');
assert.equal(second, 'Second!');

どちらも、おおよそ次のコードと同等です

first.mjs:

export let first;
export const promise = (async () => { // (A)
  const response = await fetch('http://example.com/first.txt');
  first = await response.text();
})();

main.mjs:

import {promise as firstPromise, first} from './first.mjs';
import {promise as secondPromise, second} from './second.mjs';
export const promise = (async () => { // (B)
  await Promise.all([firstPromise, secondPromise]); // (C)
  assert.equal(first, 'First content!');
  assert.equal(second, 'Second content!');
})();

モジュールが非同期になるのは、次の場合です

  1. トップレベルで await を直接使用している場合 (first.mjs)。
  2. 1つ以上の非同期モジュールをインポートしている場合 (main.mjs)。

各非同期モジュールは、本体が実行された後に完了するPromiseをエクスポートします(A行とB行)。その時点で、そのモジュールのエクスポートに安全にアクセスできます。

(2)の場合、インポートするモジュールは、すべてのインポートされた非同期モジュールのPromiseが完了するまで待機してから、本体に入ります(C行)。同期モジュールは通常どおり処理されます。

await で拒否された場合や、同期的な例外は、非同期関数と同じように処理されます。

27.14.3 トップレベルの await の長所と短所

トップレベルの await の2つの最も重要な利点は次のとおりです

短所としては、トップレベルの await は、インポートするモジュールの初期化を遅らせます。したがって、控えめに使用するのが最適です。時間がかかる非同期タスクは、後で必要に応じて実行する方が良いでしょう。

ただし、トップレベルの await を使用していないモジュールでさえ、インポートする側をブロックする可能性があります(例えば、トップレベルでの無限ループなど)。したがって、ブロックすること自体が反対の理由にはなりません。

27.15 ポリフィル:ネイティブのWebプラットフォーム機能のエミュレーション (高度)

  バックエンドにもポリフィルがあります

このセクションは、フロントエンド開発とWebブラウザーに関するものですが、同様の考え方はバックエンド開発にも当てはまります。

ポリフィルは、JavaScriptでWebアプリケーションを開発する際に直面する課題を解決するのに役立ちます。

Webプラットフォームの機能Xがあると仮定します

Webアプリケーションが起動するたびに、まず、すべての場所で利用できない可能性のある機能に対するすべてのポリフィルを実行する必要があります。その後、それらの機能がネイティブで利用可能であることが保証されます。

27.15.1 このセクションの情報源

  クイズ

クイズアプリを参照してください。