ECMAScriptモジュール

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

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

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

ひとつのJavaScriptモジュールはひとつのJavaScriptファイルに対応します。 モジュールは変数や関数などを外部にエクスポートできます。また、別のモジュールで宣言された変数や関数などをインポートできます。 この章では ECMAScriptモジュール(ESモジュール、JSモジュールとも呼ばれる) について見ていきます。 ECMAScriptモジュールは、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); // => "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"

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

次に、デフォルトエクスポート/インポートについて見ていきましょう。 デフォルトエクスポートは、モジュールごとにひとつしかエクスポートできない特殊なエクスポートです。 次の例は、すでに宣言されている変数をデフォルトエクスポートしています。 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";
// ./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

import { foo } from "./my-module.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アプリのユースケースを参照してください。