はじめに#
JavaScriptのコードが大規模になると、1つのファイルにすべてを書くのは管理が難しくなります。そこで登場するのが「ES Modules(ESモジュール)」です。ES Modulesを使うと、コードを複数のファイルに分割し、必要な機能だけをimport/exportで読み込んで再利用できます。
本記事では、以下の内容を初心者向けにわかりやすく解説します。
- ES Modulesの概要とメリット
exportの種類(名前付きエクスポート・デフォルトエクスポート)
importの基本構文と応用
- ファイル分割の実践例
- 循環参照(Cyclic Import)の注意点
- HTMLでのモジュール読み込み方法
ES Modulesとは#
ES Modules(ECMAScript Modules)は、ES2015(ES6)で導入されたJavaScriptの公式モジュールシステムです。それ以前はCommonJS(Node.js向け)やAMD(RequireJS)などの非公式な仕組みが使われていましたが、ES Modulesはブラウザとサーバーサイドの両方でネイティブにサポートされています。
ES Modulesのメリット#
ES Modulesを使うことで、以下のメリットが得られます。
| メリット |
説明 |
| コードの分割と整理 |
機能ごとにファイルを分けることで、見通しがよくなる |
| 再利用性の向上 |
共通の関数やクラスを複数のファイルから呼び出せる |
| 名前空間の分離 |
モジュールごとにスコープが分かれ、グローバル汚染を防げる |
| 依存関係の明確化 |
どのモジュールが何に依存しているかが一目でわかる |
| ツールとの連携 |
バンドラー(webpack、Vite)やトランスパイラ(Babel)との相性が良い |
export(エクスポート)の基本#
モジュールから外部に公開したい関数・変数・クラスは、exportキーワードを使ってエクスポートします。エクスポートには「名前付きエクスポート」と「デフォルトエクスポート」の2種類があります。
名前付きエクスポート(Named Export)#
名前付きエクスポートは、複数の値を個別の名前で公開する方法です。
1
2
3
4
5
6
7
8
9
10
11
12
|
// mathUtils.js
// 方法1: 個別にexportを付ける
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
|
または、ファイル末尾でまとめてエクスポートすることもできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// mathUtils.js
const PI = 3.14159;
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// 方法2: まとめてエクスポート
export { PI, add, subtract };
|
デフォルトエクスポート(Default Export)#
デフォルトエクスポートは、モジュールのメイン機能を1つだけ公開する方法です。1つのモジュールにつき、デフォルトエクスポートは1つだけ定義できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// Calculator.js
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) {
throw new Error("0で割ることはできません");
}
return a / b;
}
}
export default Calculator;
|
匿名関数をデフォルトエクスポートすることも可能です。
1
2
3
4
5
|
// greet.js
export default function (name) {
return `こんにちは、${name}さん!`;
}
|
名前付きエクスポートとデフォルトエクスポートの併用#
1つのモジュールで両方を同時に使うこともできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// userUtils.js
// デフォルトエクスポート
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
// 名前付きエクスポート
export function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
export const MAX_USERNAME_LENGTH = 50;
|
import(インポート)の基本#
importを使って、他のモジュールからエクスポートされた機能を読み込みます。
名前付きインポート#
名前付きエクスポートを読み込む場合は、中括弧{}でエクスポート名を指定します。
1
2
3
4
5
6
7
|
// main.js
import { PI, add, subtract } from "./mathUtils.js";
console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
|
デフォルトインポート#
デフォルトエクスポートを読み込む場合は、中括弧なしで任意の名前を付けられます。
1
2
3
4
5
6
7
|
// main.js
import Calculator from "./Calculator.js";
const calc = new Calculator();
console.log(calc.add(10, 5)); // 15
console.log(calc.multiply(4, 3)); // 12
|
両方を同時にインポート#
デフォルトエクスポートと名前付きエクスポートを同時に読み込むこともできます。
1
2
3
4
5
6
7
8
9
|
// main.js
import User, { validateEmail, MAX_USERNAME_LENGTH } from "./userUtils.js";
const user = new User("太郎", "taro@example.com");
console.log(user.name); // 太郎
console.log(validateEmail("taro@example.com")); // true
console.log(MAX_USERNAME_LENGTH); // 50
|
エイリアス(別名)を使ったインポート#
名前の衝突を避けたい場合や、より分かりやすい名前を付けたい場合はasキーワードを使います。
1
2
3
4
5
6
|
// main.js
import { add as addition, subtract as subtraction } from "./mathUtils.js";
console.log(addition(10, 5)); // 15
console.log(subtraction(10, 5)); // 5
|
モジュール全体をオブジェクトとしてインポート#
* as構文を使うと、モジュールのすべてのエクスポートを1つのオブジェクトにまとめて読み込めます。
1
2
3
4
5
6
7
|
// main.js
import * as MathUtils from "./mathUtils.js";
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.subtract(5, 3)); // 2
|
この方法は、エクスポートが多いモジュールを使う際に便利です。
ファイル分割の実践例#
実際のプロジェクトでのファイル分割例を見てみましょう。ECサイトの商品管理機能を想定します。
ディレクトリ構造#
1
2
3
4
5
6
7
|
project/
├── index.html
├── main.js
└── modules/
├── Product.js
├── Cart.js
└── utils.js
|
modules/Product.js#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 商品クラス
export class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
getInfo() {
return `${this.name}: ¥${this.price.toLocaleString()}`;
}
}
// 税込み価格を計算するユーティリティ
export function calculateTax(price, taxRate = 0.10) {
return Math.floor(price * (1 + taxRate));
}
|
modules/Cart.js#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
import { calculateTax } from "./utils.js";
export class Cart {
constructor() {
this.items = [];
}
addItem(product, quantity = 1) {
const existingItem = this.items.find((item) => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ product, quantity });
}
}
removeItem(productId) {
this.items = this.items.filter((item) => item.product.id !== productId);
}
getTotal() {
const subtotal = this.items.reduce((sum, item) => {
return sum + item.product.price * item.quantity;
}, 0);
return calculateTax(subtotal);
}
getItemCount() {
return this.items.reduce((count, item) => count + item.quantity, 0);
}
}
|
modules/utils.js#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 共通ユーティリティ関数
export function calculateTax(price, taxRate = 0.10) {
return Math.floor(price * (1 + taxRate));
}
export function formatCurrency(amount) {
return `¥${amount.toLocaleString()}`;
}
export function generateId() {
return Math.random().toString(36).substring(2, 9);
}
|
main.js#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { Product } from "./modules/Product.js";
import { Cart } from "./modules/Cart.js";
import { formatCurrency } from "./modules/utils.js";
// 商品を作成
const apple = new Product(1, "りんご", 150);
const banana = new Product(2, "バナナ", 100);
const orange = new Product(3, "みかん", 80);
// カートに追加
const cart = new Cart();
cart.addItem(apple, 3);
cart.addItem(banana, 2);
cart.addItem(orange, 5);
// 結果を表示
console.log("=== カートの中身 ===");
cart.items.forEach((item) => {
console.log(`${item.product.name} x ${item.quantity}`);
});
console.log(`合計点数: ${cart.getItemCount()}点`);
console.log(`合計金額(税込): ${formatCurrency(cart.getTotal())}`);
|
動的インポート(Dynamic Import)#
通常のimport文は静的にモジュールを読み込みますが、import()関数を使うと実行時に動的にモジュールを読み込めます。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 条件に応じてモジュールを読み込む
async function loadModule(moduleName) {
if (moduleName === "math") {
const module = await import("./modules/mathUtils.js");
console.log(module.add(5, 3)); // 8
} else if (moduleName === "cart") {
const { Cart } = await import("./modules/Cart.js");
const cart = new Cart();
console.log(cart);
}
}
loadModule("math");
|
動的インポートは、以下のようなケースで特に有効です。
- 初期読み込みを軽くしたいとき(遅延読み込み)
- ユーザーの操作に応じて機能を読み込むとき
- 条件分岐でモジュールを切り替えるとき
循環参照(Cyclic Import)の注意点#
循環参照とは、モジュールAがモジュールBをインポートし、モジュールBがモジュールAをインポートする状態のことです。
graph LR
A[moduleA.js] -->|import| B[moduleB.js]
B -->|import| A循環参照の問題例#
1
2
3
4
5
|
// moduleA.js
import { b } from "./moduleB.js";
export const a = "A";
console.log(b); // この時点でbはまだ初期化されていない可能性がある
|
1
2
3
4
5
|
// moduleB.js
import { a } from "./moduleA.js";
export const b = "B";
console.log(a); // この時点でaはまだ初期化されていない可能性がある
|
循環参照を回避する方法#
循環参照を避けるための一般的なアプローチは以下のとおりです。
- 共通モジュールへの分離: 共有するコードを第三のモジュールに移動する
- 依存関係の見直し: 設計を見直し、一方向の依存関係に修正する
- 遅延アクセス: 変数に直接アクセスせず、関数を通じてアクセスする
1
2
3
4
5
6
7
8
9
10
11
|
// 解決策: 共通モジュールに分離する
// shared.js
export const sharedValue = "共有データ";
// moduleA.js
import { sharedValue } from "./shared.js";
export const a = `A: ${sharedValue}`;
// moduleB.js
import { sharedValue } from "./shared.js";
export const b = `B: ${sharedValue}`;
|
HTMLでモジュールを読み込む#
ブラウザでES Modulesを使う場合は、<script>タグにtype="module"属性を付けます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ES Modules デモ</title>
</head>
<body>
<h1>ES Modules テスト</h1>
<div id="output"></div>
<!-- type="module" でモジュールとして読み込む -->
<script type="module" src="./main.js"></script>
<!-- インラインモジュールも可能 -->
<script type="module">
import { formatCurrency } from "./modules/utils.js";
console.log(formatCurrency(1000)); // ¥1,000
</script>
</body>
</html>
|
モジュールスクリプトの特徴#
type="module"を指定したスクリプトには、通常のスクリプトとは異なる特徴があります。
| 特徴 |
説明 |
| 自動的にdefer |
モジュールはHTMLパース後に実行される |
| 厳格モード |
自動的にstrict modeが適用される |
| スコープの分離 |
トップレベル変数はグローバルに漏れない |
| 一度だけ実行 |
同じモジュールは複数回読み込んでも1回だけ実行 |
| CORS制約 |
file://プロトコルでは動作しない(ローカルサーバーが必要) |
ローカルでのテスト方法#
モジュールをローカルでテストする場合は、ローカルサーバーが必要です。以下のいずれかの方法を使います。
1
2
3
4
5
6
7
|
# Node.jsのhttp-serverを使う場合
npx http-server .
# Pythonを使う場合
python -m http.server 8000
# VS Codeの拡張機能「Live Server」を使う
|
モジュールの再エクスポート(Re-export)#
複数のモジュールを1つのエントリポイントにまとめることで、インポートを簡潔にできます。
1
2
3
4
5
6
|
// modules/index.js(バレルファイル)
// 他のモジュールをまとめて再エクスポート
export { Product, calculateTax } from "./Product.js";
export { Cart } from "./Cart.js";
export { formatCurrency, generateId } from "./utils.js";
|
1
2
3
4
|
// main.js
// 1つのファイルからすべてインポートできる
import { Product, Cart, formatCurrency } from "./modules/index.js";
|
まとめ#
ES Modulesを使うことで、JavaScriptのコードを効率的に分割・管理できます。本記事で解説した内容を振り返ります。
- exportでモジュールから機能を公開(名前付き/デフォルト)
- importで他モジュールの機能を読み込む
- ファイル分割で機能ごとにコードを整理
- 循環参照には注意し、共通モジュールへの分離で回避
- HTMLでの読み込みは
type="module"を指定
- 動的インポートで必要なときだけモジュールを読み込む
モダンなJavaScript開発では、ES Modulesは必須の知識です。React、Vue、Angularなどのフレームワークも、すべてES Modulesをベースに構築されています。ぜひ実際のプロジェクトで活用してみてください。
参考リンク#