JavaScriptモジュール(ES modules)
2023/01/19更新
目次
概要
ECMAScript 2015でモジュール機能が導入され、ついにJavaScriptでも言語仕様としてモジュールが使えるようになった。従来からNode.jsなどで存在していたモジュールに類似する機能と区別して「ES modules」と呼ばれることがある。モジュールを使うと、機能を分割したり依存関係を分かりやすくしたりできる。
モジュールの作成
基本
モジュールとは、単なる1つのJavaScriptファイルである。モジュールの拡張子は従来と同じく.jsが使われるほか、モジュールであることを区別するために.mjsが使われることもある。.mjsを使う場合は、サーバーが正しくJavaScriptのMIME Typeで返せるか注意が必要である。
モジュールの中では従来と同じく通常のJavaScriptコードが書ける。その中で、外に出したい(他のファイルから使えるようにしたい)機能にexportキーワードをつけることで、その機能をエクスポートすることができる。
export var hoge1 = '~'; // varで宣言した変数をエクスポート
export let hoge2 = '~'; // letで宣言した変数をエクスポート
export const hoge3 = '~'; // constで宣言した変数をエクスポート
export function func1() { ~ } // 関数をエクスポート
export class Hoge { ~ } // クラスをエクスポート
export { ~ }を使うと複数の機能を一度にエクスポートできる。これを用いてモジュールの最後に一つだけexport文を書くと、どの機能がエクスポートされたかが分かりやすい。
const hoge = '~';
const fuga = '~';
function func() { ~ }
// 変数hogeと関数funcをエクスポート
export { hoge, func };
// 変数fugaをエクスポート
// ※1つだけの場合も波カッコは省略できないので注意。
export { fuga };
export文は、ファイル内の最上位の場所にしか書けない。つまり、ifやfor、functionなどの中で使うことはできない。
デフォルトエクスポート
モジュールからエクスポートするものがただ1つだけの場合は、export defaultを使うことでデフォルトエクスポートをすると便利である。デフォルトエクスポートをすると、後述のインポート時に表記が多少簡略化される。
const hoge = '~'; // 変数hogeをデフォルトエクスポート // ※通常のexportと異なり波カッコが無いことに注意。 export default hoge;
関数とクラスの場合は、定義と同時にデフォルトエクスポートすることもできる。
// Hogeクラスを定義すると同時にデフォルトエクスポート
export default class Hoge {
constructor() { ~ }
}
また、無名関数や無名クラスも直接デフォルトエクスポートできる。
// 無名クラスを定義すると同時にデフォルトエクスポート
export default class {
constructor() { ~ }
}
名前の変更
通常のエクスポートの場合は変数や関数の名前がそのまま機能の名前として使われるが、asを使うとこれを変更してエクスポートできる。
const obj1 = { ~ };
const obj2 = { ~ };
// obj1をMyObject1、obj2をMyObject2という名前でエクスポート
export { obj1 as MyObject1, obj2 as MyObject2 };
デフォルトエクスポートの場合は必ずdefaultという名前になるので、asで変更することはできない。
再エクスポート
他のモジュールのエクスポートを、そのまま自身のエクスポートにすることができる。
// MyModule1.js
export const hoge = 'hoge';
// MyModule2.js
export { hoge } from './MyModule1.js'
この構文を使うと、複数のモジュールを1つに集約することで、インポート時の利便性を上げることができる。
なお、この場合、MyModule2.jsの中ではhogeは使えない。あくまで、MyModule2.jsからのエクスポートにMyModule1.jsのhogeが加わるだけとなる。
モジュールの読み込み
基本
作成したモジュールを、importキーワードによってインポートすることができる。
// MyModule.jsがエクスポートした機能aaa、bbb、cccをインポート
import { aaa, bbb, ccc } from './MyModule.js';
console.log(aaa);
fromの後には、そのファイルからモジュールのファイルまでの相対パスを指定する。
インポートした機能は、その名前の変数に最初から入っていたかのように使用することができる。ただし、モジュール側でvarやletで宣言された変数であっても、インポートした側はconstで宣言した変数と同様に代入が不可能となる。
インポートした変数への直接代入はできないが、オブジェクトの場合はプロパティを変更することはできる。さらに、この変更は同じモジュールをインポートしている別のモジュールと共有される。
// MyModule.js
export const obj = { value: 'hoge' };
// Hoge.js
import { obj } from './MyModule.js';
console.log(obj.value); // 'hoge';
obj.value = 'fuga';
console.log(obj.value); // 'fuga'; ※objは変更できないが、objのプロパティは変更できる。
// Fuga.js
import { obj } from './.MyModule.js';
console.log(obj.value); // 'fuga'; ※Hoge.jsが行なった変更が共有されている。
名前の変更
エクスポート時と同様にasを使うことで名前を変更してインポートできる。複数のモジュールでインポートする名前が衝突する場合はエラーとなってしまうので、asによる名前変更が必要となる。
// MyModule.jsがエクスポートしたobj1をMyObject1、obj2をMyObject2としてインポート
import { obj1 as MyObject1, obj2 as MyObject2 } from './MyModule.js';
デフォルトエクスポートのインポート
デフォルトエクスポートされている場合は、import文の波カッコが不要となる。
// MyClass.jsがデフォルトエクスポートした機能をMyClassとしてインポート import MyClass from './MyClass.js';
この構文は、import { default as MyClass } from ~(=defaultという名前でエクスポートされた機能をMyClassとしてインポート)の簡略表記である。デフォルトエクスポートがあっても、他に通常のエクスポートが存在する場合もあるので、その場合はこのようにdefault asを使ってインポートする。
モジュールオブジェクトの作成
名前の衝突を避けるもう一つの方法として、エクスポートされている機能を全て1つのオブジェクトにまとめてインポートする方法がある。これならオブジェクトが名前空間の代わりとなるので、他のモジュールとの名前の衝突を心配する必要が無くなる。
// MyModule.jsがエクスポートした機能を全てMyModuleオブジェクトにインポート import * as MyModule from './MyModule.js'; // 例えば、MyModule.jsがobj1としてエクスポートした機能は、MyModule.obj1に格納される。 console.log(MyModule.obj1);
その他
ES2020からは、動的なインポートがサポートされた。このimport()関数は、import文とは異なり、後述のtype="module"が無い<script>タグ内でも使える。
import('./MyModule.js').then((MyModule) => {
console.log(MyModule.obj1);
});
HTMLでの使用
type="module"
HTMLで、モジュールを使ったJavaScriptを読み込む場合、<script>タグにtype="module"の指定が必要となる。
<script type="module" src="hoge.js"></script> <!-- hoge.jsの中でインポートされているファイルの<script>タグは必要ない。-->
インラインで記述する場合も、import文を使う場合はtype="module"が必要である。
<script type="module">
import { obj } from './MyModule.js';
console.log(obj);
</script>
<!-- MyModule.jsやその中でインポートされているファイルの<script>タグは必要ない。 -->
読み込み時の挙動
type="module"が指定された<script>タグは、defer属性が指定されたときと同様の読み込みになる。つまり、HTMLの読み込みがブロックされることなく、非同期でJavaScriptのファイルがロードされ、HTMLのパースが全て完了した後に実行される。実行の順序は保持される。このため、defer属性を付ける必要がない。
また、この挙動により、必ずHTMLが全て準備できた状態で実行されるため、DOMContentLoadedイベントなどを使用する必要が無くなる。一方で、先にHTMLが見えてしまうため、「読み込み中」などの表示が必要になる可能性もある。
<!-- type="module"の<script>タグ -->
<script type="module">
console.log('module!');
// HTMLのロードが終わった後に実行されるので、document.bodyが見える。
console.log(document.body.innerText); // →「コンテンツ」
</script>
<!-- 通常の<script>タグ -->
<script>
console.log('normal!');
// HTMLのロードがブロックされるので、document.bodyはまだ存在しない。
console.log(document.body.innerText); // →エラー
</script>
<body>
コンテンツ
</body>
上記のHTMLを開くと、「normal!」→「module!」→「コンテンツ」の順にコンソールに表示される。type="module"の<script>タグの方が先に書かれているにもかかわらず、通常の<script>タグよりも後に実行されていることに注意されたい。なお、通常の<script>タグのdocument.body.innerTextはエラーとなる。
type="module"の<script>タグにasync属性を指定した場合は、同様に非同期にJavaScriptファイルがロードされるが、HTMLのロード完了を待たずに準備ができた時点で実行される。インラインの場合もasync属性が指定できる。単体で完結するモジュールや、HTMLと関係なく実行できるモジュールの場合に使用できる。
未対応ブラウザ
type="module"の<script>タグは、未対応のブラウザでは無視される。逆に、<script nomodule>のようにnomodule属性をつけた<script>タグは、モジュール未対応ブラウザでのみ実行され、モジュール対応ブラウザでは無視される。
従来のJavaScriptとの違い
注意が必要な点
前節で解説したtype="module"や読み込み時の挙動などに加え、さらに以下のようなつまずきやすいポイントが存在するので注意が必要である。
モジュールは、
file://~のURLでローカルファイルを開いた場合は読み込むことができない。開発段階であっても、必ずローカルサーバーを立てるなどして、http://~またはhttps://~のURLで開く必要がある。モジュールの内部は自動的にstrictモードとなる。
モジュールは、複数のファイルからインポートされていたり、同じファイルが複数の
<script type="module">で読み込まれたりしていても、一度しかロードおよび実行されない。<script>タグのsrc属性で、異なるオリジンのファイルを指定する場合はCORS対応が必要となる。つまり、サーバー側がAccess-Control-Allow-Originヘッダを付ける必要が出てくる。
モジュールのスコープ
各モジュール内には、最上位に独自のスコープがある。このスコープは他のモジュールからは見えない。最上位でのthisは常にundefinedとなる。
<script type="module">
const hoge = 'module';
// モジュールなので、変数hogeはこの<script>タグ内だけの独自スコープとなる。
</script>
<script>
const fuga = 'normal';
// 従来のJavaScriptなので、変数fugaはグローバル変数となる。
</script>
<script type="module">
console.log(fuga); // 'normal'
console.log(hoge); // エラー
</script>
<script>
window.onload = function() {
console.log(fuga); // 'normal'
console.log(hoge); // エラー
};
</script>
ただし、ブラウザではwindowやdocumentなどのグローバルなブラウザオブジェクトはどのモジュールでも共通して使える。