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
などのグローバルなブラウザオブジェクトはどのモジュールでも共通して使える。