[ES2015] ECMAScriptモジュール

モジュールとは、変数や関数などをまとめたものです。 JavaScriptにおいては、1つのモジュールは1つのJavaScriptファイルに対応します。

モジュールについてはNode.jsでCLIアプリのユースケースTodoアプリのユースケースで実際に動かしながら学ぶため、ここでは構文の説明とモジュールのイメージをつかむのが目的です。 この章のサンプルコードを実際に動かすためにはローカルサーバーなどの準備が必要です。 そのため、ユースケースの章を先に読んでから戻ってきてもかまいません。

モジュールは、保守性・名前空間・再利用性のために使われます。

  • 保守性: 依存性の高いコードの集合を一箇所にまとめ、それ以外のモジュールへの依存性を減らせます
  • 名前空間: モジュールごとに分かれたスコープがあり、グローバルの名前空間を汚染しません
  • 再利用性: 便利な変数や関数を複数の場所にコピーアンドペーストせず、モジュールとして再利用できます

モジュールは変数や関数などをモジュール外部にエクスポートできます。また、モジュールからエクスポートされた変数や関数などをインポートして利用できます。 モジュールに処理を分けることで、コードの見通しが良くなったり、特定のことに関する処理をモジュールにまとめたり、処理を再利用できるようになります。 それによって、コードの行数が増えてきた場合にも、一度にみるコードの量をモジュールで分割できるようになり、メンテナンス性がよくなります。

この章では、ECMAScriptモジュール(ESモジュール、JavaScriptモジュールとも呼ばれる) について見ていきます。 ECMAScriptモジュールは、ES2015で導入されたJavaScriptファイルをモジュール化する言語標準の機能です。

ECMAScriptモジュールの構文

ECMAScriptモジュールは、export文によって変数や関数などをエクスポートできます。 また、import文を使って別のモジュールからエクスポートされたものをインポートできます。 インポートとエクスポートはそれぞれに 名前つきデフォルト という2種類の方法があります。

まずは名前つきエクスポート/インポート文について見ていきましょう。

名前つきエクスポート/インポート

名前つきエクスポートは、モジュールごとに複数の変数や関数などをエクスポートできます。 次の例では、foo変数とbar関数をそれぞれ名前つきエクスポートしています。 export文のあとに続けて{}を書き、その中にエクスポートする変数を入れることで、宣言済みの変数を名前つきエクスポートできます。

named-export.js

const foo = "foo";
// 宣言済みのオブジェクトを名前つきエクスポートする
export { foo };

また、名前つきエクスポートではexport文を宣言の前につけると、宣言と同時に名前つきエクスポートできます。

named-export-declare.js

// 宣言と同時に名前つきエクスポートする
export function bar() { };

名前つきインポートは、指定したモジュールから名前を指定して選択的にインポートできます。 次の例では my-module.jsから名前つきエクスポートされたオブジェクトの名前を指定して名前つきインポートしています。 import文のあとに続けて{}を書き、その中にインポートしたい名前つきエクスポートの名前を入れます。 複数の値をインポートしたい場合は、それぞれの名前をカンマで区切ります。

my-module.js

export const foo = "foo";
export function bar() { }

named-import.js

// 名前つきエクスポートされたfooとbarをインポートする
import { foo, bar } from "./my-module.js";
console.log(foo); // => "foo"
console.log(bar); // => function bar()

名前つきエクスポート/インポートのエイリアス

名前つきエクスポート/インポートにはエイリアスの仕組みがあります。 エイリアスを使うと、宣言済みの変数を違う名前で名前つきエクスポートできます。 エイリアスをつけるには、次のようにasのあとにエクスポートしたい名前を記述します。

named-export-alias.js

const internalFoo = "foo";
// internalFoo変数をfooとして名前つきエクスポートする
export { internalFoo as foo };

また、名前つきインポートしたオブジェクトにも別名をつけることができます。 インポートでも同様に、asのあとに別名を記述します。

named-import-alias.js

// fooとして名前つきエクスポートされた変数をmyFooとしてインポートする
import { foo as myFoo } from "./named-export-alias.js";
console.log(myFoo); // => "foo"

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

次に、デフォルトエクスポート/インポートについて見ていきましょう。 デフォルトエクスポートは、モジュールごとに1つしかエクスポートできない特殊なエクスポートです。 次の例は、すでに宣言されている変数をデフォルトエクスポートしています。 export default文で、後に続く式の評価結果をデフォルトエクスポートします。

default-export.js

const foo = "foo";
// foo変数の値をデフォルトエクスポートする
export default foo;

また、export文を宣言の前につけると、宣言と同時にデフォルトエクスポートできます。 このとき関数やクラスの名前を省略できます。

// 宣言と同時に関数をデフォルトエクスポートする
export default function() {}

ただし、変数宣言は宣言とデフォルトエクスポートを同時に行うことはできません。 なぜなら、変数宣言はカンマ区切りで複数の変数を定義できてしまうためです。 次の例は実行できない不正なコードです。

// 変数宣言と同時にデフォルトエクスポートはできない
export default const foo = "foo", bar = "bar";

デフォルトインポートは、指定したモジュールのデフォルトエクスポートに名前をつけてインポートします。 次の例では my-module.jsのデフォルトエクスポートにmyModuleという名前をつけてインポートしています。 import文のあとに任意の名前をつけることで、デフォルトエクスポートをインポートできます。

my-module.js

export default {
    baz: "baz"
};

default-import.js

// デフォルトエクスポートをmyModuleとしてインポートする
import myModule from "./my-module.js";
console.log(myModule); // => { baz: "baz" }

実はデフォルトエクスポートは、defaultという固有の名前による名前つきエクスポートと同じものです。 そのため、名前つきエクスポートでas defaultとエイリアスをつけることでデフォルトエクスポートすることもできます。

default-export-alias.js

const foo = "foo";
// foo変数の値をデフォルトエクスポートする
export { foo as default };

同様に、名前つきインポートにおいてもdefaultという名前がデフォルトインポートに対応しています。 次のように、名前つきインポートでdefaultを指定するとデフォルトインポートできます。 ただし、defaultは予約語なので、この方法では必ずas構文を使ってエイリアスをつける必要があります。

default-import-alias.js

// デフォルトエクスポートをmyModuleとしてインポートする
import { default as myModule } from "./my-module.js";
console.log(myModule); // => { baz: "baz" }

また、名前つきインポートとデフォルトインポートの構文は同時に記述できます。 次のように2つの構文をカンマでつなげます。

default-import-with-named.js

// myModuleとしてデフォルトインポートし、
// fooを名前つきインポートする
import myModule, { foo } from "./my-module.js";
console.log(foo); // => "foo"
console.log(myModule); // => { baz: "baz" }

ECMAScriptモジュールでは、エクスポートされていないものはインポートできません。 なぜならECMAScriptモジュールはJavaScriptのパース段階で依存関係が解決され、インポートする対象が存在しない場合はパースエラーとなるためです。 デフォルトインポートは、インポート先のモジュールがデフォルトエクスポートをしている必要があります。 同様に名前つきインポートは、インポート先のモジュールが指定した名前つきエクスポートをしている必要があります。

その他の構文

ECMAScriptモジュールには名前つきとデフォルト以外にもいくつかの構文があります。

再エクスポート

再エクスポートとは、別のモジュールからインポートしたものを、改めて自分自身からエクスポートし直すことです。 複数のモジュールからエクスポートされたものをまとめたモジュールを作るときなどに使われます。

再エクスポートは次のようにexport文のあとにfromを続けて、別のモジュール名を指定します。

// ./my-module.jsのすべての名前つきエクスポートを再エクスポートする
export * from "./my-module.js";
// [ES2020] ./my-module.jsのすべての名前つきエクスポートを名前空間オブジェクトとして再エクスポートする
export * as myNameSpace from "./my-module.js";
// ./my-module.jsの名前つきエクスポートを選んで再エクスポートする
export { foo, bar } from "./my-module.js";
// ./my-module.jsの名前つきエクスポートにエイリアスをつけて再エクスポートする
export { foo as myModuleFoo, bar as myModuleBar } from "./my-module.js";
// ./my-module.jsのデフォルトエクスポートをデフォルトエクスポートとして再エクスポートする
export { default } from "./my-module.js";
// ./my-module.jsのデフォルトエクスポートを名前つきエクスポートとして再エクスポートする
export { default as myModuleDefault } from "./my-module.js";
// ./my-module.jsの名前つきエクスポートをデフォルトエクスポートとして再エクスポートする
export { foo as default } from "./my-module.js";

すべてをインポート

import * as構文は、すべての名前つきエクスポートをまとめてインポートします。 この方法では、モジュールごとの 名前空間 となるオブジェクトを宣言します。 エクスポートされた変数や関数などにアクセスするには、その名前空間オブジェクトのプロパティを使います。 また、先ほどのとおり、default という固有名を使うとデフォルトエクスポートにもアクセスできます。

my-module.js

export const foo = "foo";
export function bar() { }
export default {
    baz: "baz"
};

namespace-import.js

// すべての名前つきエクスポートをmyModuleオブジェクトとしてまとめてインポートする
import * as myModule from "./my-module.js";
// fooとして名前つきエクスポートされた値にアクセスする
console.log(myModule.foo); // => "foo"
// defaultとしてデフォルトエクスポートされた値にアクセスする
console.log(myModule.default); // => { baz: "baz" }

副作用のためのインポート

モジュールの中には、グローバルのコードを実行するだけで何もエクスポートしないものがあります。 たとえば次のような、グローバル変数を操作するためのモジュールなどです。

side-effects.js

// グローバル変数を操作する(副作用)
window.foo = "foo";

このようなモジュールをインポートするには、副作用のためのインポート構文を使います。 この構文では、指定したモジュールを読み込んで実行するだけで、何もインポートしません。

// ./side-effects.jsのグローバルコードが実行される
import "./side-effects.js";

ECMAScriptモジュールを実行する

作成したECMAScriptモジュールを実行するためには、起点となるJavaScriptファイルをECMAScriptモジュールとしてウェブブラウザに読み込ませる必要があります。 ウェブブラウザはscript要素によってJavaScriptファイルを読み込み、実行します。 次のようにscript要素にtype="module"属性を付与すると、ウェブブラウザはJavaScriptファイルをECMAScriptモジュールとして読み込みます。

<!-- my-module.jsをECMAScriptモジュールとして読み込む -->
<script type="module" src="./my-module.js"></script>
<!-- インラインでも同じ -->
<script type="module">
import { foo } from "./my-module.js";
</script>

type="module"属性が付与されない場合は通常のスクリプトとして扱われ、ECMAScriptモジュールの機能は使えません。 スクリプトとして読み込まれたJavaScriptでimport文やexport文を使用すると、構文エラーが発生します。

ウェブブラウザの環境では、インポートされるモジュールの取得はネットワーク経由で解決されます。 そのため、モジュール名はJavaScriptファイルの絶対URLあるいは相対URLを指定します。 詳しくはTodoアプリのユースケースを参照してください。