モジュール

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

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

ひとつのJavaScriptのモジュールはひとつのJavaScriptファイルに対応します。 モジュールは変数や関数などを外部にエクスポートできます。また、別のモジュールで宣言された変数や関数などをインポートできます。 この章では ECMAScriptモジュール(ESモジュール、JSモジュールとも呼ばれる) について見ていきます。 ECMAScriptモジュールは、JavaScriptファイルをモジュール化する言語標準の機能です。

ESモジュール

ESモジュールは、export文によって変数や関数などをエクスポートできます。 また、import文を使って別のモジュールからエクスポートされたものをインポートできます。

まずはexport文について見ていきましょう。

export文

export文は変数や関数などをエクスポートし、別のモジュールから参照できるようにします。 エクスポートの方法は、 名前付きエクスポートデフォルトエクスポート の2種類があります。 名前付きエクスポートは、モジュールごとに複数の変数や関数などをエクスポートできます デフォルトエクスポートは、モジュールごとにひとつしかエクスポートできません。 それぞれについて見ていきましょう。

名前付きエクスポート

名前付きエクスポートには、すでに宣言した変数名と同じ名前でエクスポートする方法と、異なる名前でエクスポートする方法の2つがあります。 次の例は、すでに宣言されている変数をエクスポートする構文です。 export文のあとに続けて{}を書き、その中にエクスポートする変数を入れます。

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

また、次のように、宣言とエクスポートを同時に行うこともできます。

// 変数の宣言のみ
export let foo; // varも使用可
// 宣言と代入
export const bar = "bar"; // var, letも使用可
// 関数の宣言
export function fn() { }
// クラスの宣言
export class ClassName { }

もうひとつの方法は、エクスポートする対象に エイリアスをつけて、宣言した変数名と違う名前でエクスポートする構文です。 エクスポートする時にエイリアスをつけるには、次のような構文を使います。asのあとにエクスポートしたい名前を記述します。

const internalFoo = "foo";
function internalBar() { };
export { internalFoo as foo, internalBar as bar };

デフォルトエクスポート

デフォルトエクスポートには、export default文を使ってエクスポートする方法と、エイリアスにdefaultを指定して名前付きエクスポートする方法の2つがあります。

export default文は、後に続く式の評価結果をデフォルトエクスポートします。 次の例では、すでに宣言されている変数をデフォルトエクスポートします。

const foo = "foo";
export default foo;

また、次のように関数とクラスは宣言とデフォルトエクスポートを同時に行うことができます。 このとき関数やクラスは名前を省略できます。

// 関数の宣言
export default function () {}

// クラスの宣言
export default class {}

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

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

エイリアスを使ってデフォルトエクスポートする方法では、次のようにas defaultを付与します。 defaultというエイリアスがつけられた名前付きエクスポートは、デフォルトエクスポートと同じ意味になります。

const foo = "foo";
function bar() { };
// fooは名前付き、barはデフォルトとしてエクスポートする
export { foo, bar as default };

再エクスポート

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

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

// ./myModule.jsのすべての名前付きエクスポートを再エクスポートする
export * from "./myModule.js";
// ./myModule.jsの名前付きエクスポートを選んで再エクスポートする
export { foo, bar } from "./myModule.js";
// ./myModule.jsの名前付きエクスポートにエイリアスをつけて再エクスポートする
export { foo as myModuleFoo, bar as myModuleBar } from "./myModule.js";
// ./myModule.jsのデフォルトエクスポートをデフォルトエクスポートとして再エクスポートする
export { default } from "./myModule.js";
// ./myModule.jsのデフォルトエクスポートを名前付きエクスポートとして再エクスポートする
export { default as myModuleDefault } from "./myModule.js";
// ./myModule.jsの名前付きエクスポートをデフォルトエクスポートとして再エクスポートする
export { foo as default } from "./myModule.js";

import文

import文は、別のモジュールからエクスポートされた変数や関数などを自身のモジュールにインポートします。 インポートした変数や関数は、そのモジュールの先頭で宣言されたものと同じように扱えます。

エクスポートと同じように、インポートにも 名前付きインポートデフォルトインポートの2種類があります。 それらに加え、指定したモジュールからすべてのエクスポートをまとめてインポートする方法と、副作用のためのインポートがあります。 それぞれについて見ていきましょう。

名前付きインポート

名前付きインポートは、指定したモジュールから名前を指定してインポートします。 名前を指定してインポートするには、import文のあとに続けて{}を書き、その中にインポートしたい名前付きエクスポートの名前を入れます。 次の例では、./myModule.jsモジュールから名前付きエクスポートされたfoobarをインポートしています。

// 名前付きエクスポートされたfooとbarをインポートする
import { foo, bar } from "./myModule.js";

エクスポートするときと同じように、インポートするときにも別の名前をつけることができます。

// fooとして名前付きエクスポートされた変数をmyModuleFooとしてインポートする
import { foo as myModuleFoo } from "./myModule.js";

デフォルトインポート

デフォルトインポートは、指定したモジュールのデフォルトエクスポートをインポートします。 デフォルトインポートには、専用の構文を使う方法と、名前付きインポートで default を指定する方法の2つがあります。

デフォルトインポート専用の構文では、import文のあとに任意の名前をつけてデフォルトエクスポートをインポートします。

// myModuleDefaultとしてデフォルトエクスポートをインポートする
import myModuleDefault from "./myModule.js";

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

// myModuleDefaultとしてデフォルトエクスポートをインポートする
import { default as myModuleDefault } from "./myModule.js";

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

// myModuleDefaultとしてデフォルトエクスポートをインポートし、
// 名前付きエクスポートされたfooをインポートする
import myModuleDefault, { foo } from "./myModule.js";

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

すべてをインポート

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

// すべての名前付きエクスポートをmyModuleオブジェクトとしてまとめてインポートする
import * as myModule from "./myModule.js";
// fooとして名前付きエクスポートされた変数にアクセスする
console.log(myModule.foo);

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

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

import { foo } from "./myModule.js";

// グローバル変数を操作する
window.foo = foo;

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

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

ESモジュールを実行する

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

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

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

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

CommonJSモジュール

CommonJSモジュールとは、Node.js環境で利用されているモジュール化の仕組みです。 CommonJSモジュールはESモジュールの仕様が策定されるよりもずっと古くから使われています。 Node.jsの標準パッケージやNPMで配布されるサードパーティパッケージは、CommonJSモジュールとして提供されていることがほとんどです。

CommonJSモジュールはNode.jsのグローバル変数であるmodule変数を使って変数や関数などをエクスポートします。 次のようにmodule.exportsプロパティに代入されたオブジェクトが、そのJavaScriptファイルからエクスポートされます。 複数の名前付きエクスポートが可能なESモジュールと違い、CommonJSではmodule.exportsプロパティの値だけがエクスポートの対象です。

module.exports = {
    foo: "foo"
};

モジュールをインポートするには、requireグローバル関数を使います。 次のようにrequire関数にモジュール名を渡し、戻り値としてエクスポートされたオブジェクトを受け取ります。

const myModule = require("./myModule.js");

Node.jsではESモジュールもサポートする予定ですが、現在はまだ安定した機能としてサポートされていません。

[コラム] モジュールバンドラー

モジュールバンドラーとは、JavaScriptのモジュール依存関係を解決し、複数のモジュールをひとつのJavaScriptファイルに結合するツールのことです。 モジュールバンドラーは起点となるモジュールが依存するモジュールを次々にたどり、適切な順序になるように結合(バンドル)します。

NPMによって多くのJavaScriptライブラリがNode.js向けに配布されていますが、これらはほぼすべてCommonJSモジュールです。 それらのライブラリを使ったアプリケーションをWebブラウザで実行するためには、CommonJSモジュールを解決し、ひとつのJavaScriptファイルに結合する必要がありました。 結果的に、Node.js向けでないアプリケーションもモジュール化することが一般的になり、モジュールバンドラーはJavaScript開発において無くてはならないものになりました。

モジュールバンドラーにはCommonJSだけでなくESモジュールにも対応したものもあります。 また、バンドルする際にJavaScriptコードの最適化を行うなどバンドル以外の機能をもつものもあります。 JavaScriptモジュールについてのドキュメントでは、 WebにおけるJavaScriptのモジュールと、バンドルする目的などについて詳しくまとめられています。