この記事では、JavaScript のプログラムを複数のモジュール(部品)に分割し、必要な時に取り込むための構文である import と export について解説する。
プログラミング学習未経験からでもITエンジニアとしての基礎を身に付けることができるオススメのスクール >> 【DMM WEBCAMP】
モジュール(module)とは
ソースコードを機能単位に分割して部品として扱えるようにしたものをモジュール(module)と呼ぶ。機能単位に分割して部品化することでソースコードが整理され、また再利用しやすくなるといったメリットがある。
JavaScript においては、1つのモジュールを1つのファイルで作成する。
ES Modules と CommonJS
ECMAScript の仕様に基づいたモジュールを管理する仕組みのことを ES Modules という(ESM と省略して表現することもある)。ES Modules は、ES6 (ES2015) で導入された。
その他にも JavaScript のモジュールを管理する仕組みとして、CommonJS(CJS)と呼ばれるものがある。CommonJS は Node.js におけるモジュール管理の仕組みであり、Node.js が開発されたときに JavaScript にモジュールを管理する仕組みがまだ存在しなかったために開発された。
これらのモジュール管理システムには次のような違いがある。
ES Modules | CommonJS | |
---|---|---|
使用するキーワード | import / export | require / exports |
JavaScript 実行環境 | ブラウザ (Node.js でも使用可能) | Nodo.js |
ファイルの拡張子 | .js あるいは .mjs | .js あるいは .cjs |
この記事の以下の内容は、ECMAScirpt のモジュール管理システムである ES Modules について説明したものなので注意してほしい。
import と export
JavaScript において、モジュールとは単なる1つのファイルだ。そしてモジュールを作成したり、あるいは外部のモジュールを読み込みたいときは、import と export というキーワードを使用する。
- import というキーワードを使用して、他のモジュールで定義された機能(変数や関数など)を取り込んで使用できるようにする。
- export というキーワードを使用して、作成した機能を外部に公開し、import できるようにする。
例えば、次のように moduleB.js というモジュールを作成し、その中で export キーワードを使って、変数と関数を外部に公開する。
// moduleB.js
export let value = 100;
export function hello() {
console.log('Hello');
}
そしてこのモジュール moduleB.js で定義され、かつ公開された変数・関数を別のモジュール moduleA.js で使用するためには、以下のように import キーワードを使用する。import キーワードの後に { } で使用したい変数・関数の名前を並べ、from の後にモジュールのパスをシングルクウォート、またはダブルクウォートで囲って指定する。
// moduleA.js
import {value, hello} from './moduleB.js';
console.log(value);
hello();
そしてこのモジュール moduleA.js を HTML に読み込ませ、JavaScript を実行させてみる。ただし通常の JavaScript ファイルと同じやり方で <script> タグを使用するとエラーが発生する。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ES Modules</title>
</head>
<body>
<script src="moduleA.js"></script>
</body>
</html>
「Uncaught SyntaxError: Cannot use import statement outside a module(モジュール外で import 文を使用することはできない)」というエラーが表示されている。
ブラウザの HTML から JavaScript のモジュールを読み込むためには、次のように <script> タグの type 属性の値に “module” を指定する必要がある。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ES Modules</title>
</head>
<body>
<script type="module" src="moduleA.js"></script>
</body>
</html>
これによりブラウザは JavaScript ファイル moduleA.js を ES Modules で管理されるモジュールファイルとして解析するようになる。
ただし、これでもまだエラーが発生する。
このエラーは、http リクエストなどを使用して moduleA.js を取得しろと言っているため、ローカルサーバーを立ち上げてサーバー経由で HTML を取得すればエラーは発生しなくなる。もしコードエディタとして Visual Studio Code を使用しているのなら、Live Server などの拡張機能をインストールすれば、簡易的なローカルサーバーを簡単に使用することができるので試してほしい。
そしてサーバー経由で HTML にアクセスすると、次のようにコンソールに表示される。
デフォルトのエクスポート
export の方法には「名前付きのエクスポート」と「デフォルトのエクスポート」の2種類が存在する。
名前付き export は上記のサンプルでも行なったように、export する対象の変数名や関数名を公開し、import する側でもその名前を使って使用を宣言するやり方だ。
デフォルトのエクスポートの場合は、export する対象の名前を公開する代わりに default というキーワードを付けることで、名前なしでの export を行う方法だ。default キーワードを指定できるのは1つだけであり、次のように記述する。
// moduleB.js
export let value = 100;
export function hello() {
console.log('Hello');
}
export default 'default export';
// moduleA.js
import defaultVal, {value, hello} from './moduleB.js';
console.log(value); // 100
hello(); // Hello
console.log(defaultVal); // default export
名前付きのエクスポートの場合、インポート(import)側では、{ } の中に名前を並べて使用を宣言するが、デフォルトのエクスポートの場合は { } を書かずに任意の名前を指定してインポートを行う。
様々な import の記述方法
基本的なインポートの記述は上で述べた通りだが、それ以外にもいくつかの import の記述方法がある。
別名(エイリアス)を与えてインポートする
as を使用して、インポートの際に別名を与えることもできる。その場合、そのファイル内ではインポート対象は別名でのみアクセス可能となる。
// moduleB.js
export let value = 100;
export function hello() {
console.log('Hello');
}
export default 'default export';
// moduleA.js
import defaultVal, {value as val, hello as h} from './moduleB.js';
console.log(val); // 100
h(); // Hello
console.log(defaultVal); // default export
モジュール全体をまとめてインポートする
*(アスタリスク)でモジュール内のすべての export をまとめてインポートすることができる。この場合、as を使用して名前を与える必要がある。
// moduleB.js
export let value = 100;
export function hello() {
console.log('Hello');
}
export default 'default export';
// moduleA.js
import defaultVal, * as moduleB from './moduleB.js';
console.log(moduleB.value); // 100
moduleB.hello(); // Hello
console.log(moduleB.default); // default export
console.log(defaultVal); // default export
このサンプルでは moduleB という名前を与えてまとめてインポートしている。その場合、「moduleB.~」という形式で個々の export にアクセスすることができる。また、デフォルトのエクスポートは「moduleB.default」のようにアクセスできる。
モジュールコンテキスト
モジュール内でのトップレベルのコンテキストは、グローバルコンテキストではなく、モジュールコンテキストと呼ばれる。モジュールコンテキストでも
- 実行中のコンテキスト内の変数・関数を使用できる
- グローバルオブジェクトを使用できる
ということはグローバルコンテキストと同じだが、
- this キーワードが使用できない
という点が大きく異なっている。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ES Modules</title>
</head>
<body>
<script type="module" src="moduleA.js"></script>
</body>
</html>
// moduleA.js
console.log(this); // --> undefined
function myfunc() {
console.log(this);
}
myfunc(); // --> undefined
モジュールスコープ
スクリプトファイル直下において、let や const で宣言された変数・定数が所属するスコープのことをスクリプトスコープと呼ぶが、ES Modules ではモジュールスコープと呼ばれる。
スクリプトファイル直下で let や const で宣言した変数の他に、import した変数や関数などもモジュールスコープに所属することになる。
// moduleB.js
export let value = 100;
export function hello() {
console.log('Hello');
}
export default 'default export';
// moduleA.js
import defaultVal, {value, hello} from './moduleB.js';
console.log(value); // 100
hello(); // Hello
console.log(defaultVal); // default export
<script type=”module”> の特徴
defer 属性が付与された状態になる
ブラウザが <script type=”module”> で指定されたスクリプトを実行するタイミングは、defer 属性を付与したスクリプトと同様に、HTML の構文解析が終わったタイミングとなる。
したがって、下記のサンプルのように <script type=”module”> を <h1> より前に書いたとしても、モジュールのスクリプト実行時には HTML の解析が終わっているため、<h1> 要素にアクセスすることができる。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ES Modules</title>
</head>
<body>
<script type="module" src="moduleA.js"></script>
<h1>ES Modules</h1>
</body>
</html>
// moduleA.js
const h1 = document.querySelector('h1');
console.log(h1.textContent); // --> ES Modules
同じ <script type=”module”> は一度しか実行されない
同一のファイルを指定して <script> タグを複数記述した場合、モジュールでないものは複数回実行されるが、モジュールは一度しか実行されない。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ES Modules</title>
</head>
<body>
<script src="main.js"></script>
<script src="main.js"></script>
<script src="main.js"></script>
<script type="module" src="moduleA.js"></script>
<script type="module" src="moduleA.js"></script>
<script type="module" src="moduleA.js"></script>
</body>
</html>
// main.js
console.log('main.js');
// moduleA.js
console.log('moduleA.js');
nomodule 属性
モジュールに対応していない古いブラウザに対して何か特別なスクリプトを実行させたい場合には、<script> タグに nomodule 属性を付与すればよい。
nomodule 属性を付与したスクリプトは、モジュールに対応していないブラウザでのみ実行される。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ES Modules</title>
</head>
<body>
<script nomodule>alert('module に対応していないブラウザです');</script>
<script type="module" src="moduleA.js"></script>
</body>
</html>
モジュールでは常に Strict モード
モジュール内では “use strict”; を記述しなくても常に Strict モードが適用される。次のサンプルでは、変数 msg を宣言なしで使用しているためエラーとなっている。
デフォルトのモードであれば、グローバルオブジェクト(window オブジェクト)にプロパティが追加されエラーにはならないはずだが、Strict モードが適用されているため、エラーとなっている。
// moduleA.js
function myfunc() {
msg = 'Hello';
console.log(msg);
}
myfunc();
// --> Uncaught ReferenceError: msg is not defined
ダイナミックインポート
ダイナミックインポート(動的インポート)とは、モジュールが必要になったタイミングで非同期にインポートする方法であり、import キーワードを関数のように使用することで実現できる。
import() は promise を返却する。また、import() によるダイナミックインポートは通常のスクリプトでも使用できるため、<script type=”module”> のように type=”module” を指定する必要はない。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ES Modules</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
// module.js
export let value = 100;
export function hello() {
console.log('Hello');
}
export default 'default export';
// main.js
import('./module.js').then(function(module) {
console.log('module.value:', module.value);
module.hello();
console.log('module.default:', module.default);
})
// module.value: 100
// Hello
// module.default: default export
import() が返すのは promise なため、async / await 構文を使用して、上記の main.js を次のように書くこともできる。
// main.js
(async function() {
const module = await import('./module.js');
console.log('module.value:', module.value);
module.hello();
console.log('module.default:', module.default);
})();
// module.value: 100
// Hello
// module.default: default export