JavaScriptの基礎 - ESモジュール
概要
ESモジュール (ECMAScript Modules, ESM) は、ES2015 (ES6) で導入されたJavaScriptの標準モジュールシステムである。
各ファイルが独立したスコープを持ち、export で値を公開し、import で他のモジュールの値を利用する仕組みを提供する。
ESモジュールの主な特性を以下に示す。
- strictモードの自動適用
- モジュールファイルは自動的にstrictモードで実行される。
"use strict"を明示する必要はない。
- モジュールファイルは自動的にstrictモードで実行される。
- プライベートスコープ
exportされない限り、モジュール内の宣言は外部からアクセスできない。
- 遅延実行 (defer)
- ブラウザでは、モジュールスクリプトはHTMLのパース完了後に実行される。
- 一度だけ評価
- 同一モジュールが複数回
importされても、コードは初回のみ実行されキャッシュされる。
- 同一モジュールが複数回
- 静的解析とTree Shaking
import/export文はファイルの先頭に記述し、静的に解析できる。バンドラーによるTree Shaking (未使用コードの除去) に対応する。
- トップレベルawait対応
- モジュール内では、関数の外でも
awaitを使用できる。
- モジュール内では、関数の外でも
ブラウザでは <script type="module"> と指定することでESMを利用できる。
Node.jsでは、ファイルの拡張子を .mjs にするか、package.json に "type": "module" を指定することでESMを有効化できる。
モジュールの基本
モジュールとは
モジュールとは、関連するコードを一纏めにしたファイル単位の独立した単位である。
モジュールシステムを使用することにより、コードを機能ごとに分割して管理して、再利用性と保守性を高めることができる。
ESモジュールでは、各ファイルがそれ自体のスコープを持つ。
あるモジュール内で定義した変数や関数は、明示的に export しない限り他のモジュールからは参照できない。
モジュールの特性
下表に、ESモジュールが持つ主要な特性を示す。
| 特徴 | 説明 |
|---|---|
| 自動strictモード | モジュールスコープは自動的にstrictモードとなる。 暗黙的なグローバル変数の作成やその他のstrictモード違反がエラーになる。 |
| プライベートスコープ | export されない宣言はモジュール内でのみ有効である。グローバルスコープを汚染しない。 |
| 遅延評価とキャッシュ | モジュールは、初回 import 時に評価され、以降は評価済みの結果がキャッシュとして再利用される。そのため、副作用を持つモジュールが複数箇所から import されても、副作用は1度しか実行されない。
|
| 静的な構造 | import 文と export 文は静的に解析される。コードの実行前にモジュール間の依存関係が確定するため、循環依存の検出やTree Shakingが可能になる。 |
| トップレベルawait | ESモジュールでは、非同期関数の外側 (トップレベル) でも await を使用できる。CommonJSにはない機能である。 |
エクスポート
エクスポートとは、モジュールから値を公開することである。
名前付きエクスポート
名前付きエクスポートは、1つのモジュールから複数の値をエクスポートする方法である。
エクスポートした名前がそのままインポート時の識別子となる。
- 宣言と同時にエクスポートする方法
// math.js // 宣言と同時にエクスポートする export function add(a, b) { return a + b; } export function subtract(a, b) { return a - b; } export const PI = 3.14159;
- 宣言後にまとめてエクスポートする方法
// string-utils.js function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function trim(str) { return str.trim(); } // エイリアス付きでエクスポートすることもできる function _internalHelper(str) { return str.toLowerCase(); } // まとめてエクスポートする export { capitalize, trim }; // エイリアス付きエクスポート : 内部名とは異なる名前で公開する export { _internalHelper as helper };
デフォルトエクスポート
デフォルトエクスポートは、1つのモジュールにつき1つだけ定義できるエクスポートである。
インポート時に任意の名前を付けることができるため、モジュールの主要な機能を公開する場合に適している。
デフォルトエクスポートの例を以下に示す。
- 関数のデフォルトエクスポート
// logger.js // 関数のデフォルトエクスポート export default function log(message) { console.log("[LOG]", message); }
- クラスのデフォルトエクスポート
// User.js // クラスのデフォルトエクスポート export default class User { constructor(name, email) { this.name = name; this.email = email; } toString() { return this.name + " <" + this.email + ">"; } }
- 値のデフォルトエクスポート
// config.js // 値のデフォルトエクスポート (export defaultの後に直接値を記述する) export default { apiUrl : "https://api.example.com", timeout : 5000, retries : 3 };
名前付きエクスポート と デフォルトエクスポートの比較
下表に、名前付きエクスポートとデフォルトエクスポートの違いを示す。
| 項目 | 名前付きエクスポート | デフォルトエクスポート |
|---|---|---|
| 1モジュールあたりの個数 | 複数可能 | 1つのみ |
| インポート時の名前 | エクスポート名と一致する。(as で変更可能) |
任意の名前を指定できる。 |
| インポート構文 | import { name } from 'module' | import name from 'module' |
| Tree Shaking | 容易 | 限定的 |
| リネームの必要性 | as キーワードが必要 |
インポート時に自由に命名できる。 |
| 主な用途 | ユーティリティ関数群、定数群 | モジュールの単一のメイン機能 |
インポート
名前付きインポート
名前付きエクスポートされた値は、波括弧 { } を使用してインポートする。
import { add, subtract } from './math.js';
console.log(add(10, 3)); // 13
console.log(subtract(10, 3)); // 7
デフォルトインポート
デフォルトエクスポートされた値は、波括弧なしでインポートする。
インポート時の名前は任意に決めることができる。
import log from './logger.js';
import User from './User.js';
log("アプリケーション起動"); // "[LOG]アプリケーション起動"
const user = new User("太郎", "taro@example.com");
console.log(user.toString()); // "太郎 <taro@example.com>"
名前付きインポートとデフォルトインポートを1行で組み合わせることもできる。
// デフォルトインポートと名前付きインポートを同時に行う
import React, { useState, useEffect } from 'react';
エイリアス (as)
as キーワードを使用することにより、インポートする値に別名 (エイリアス) を付けることができる。
これは、名前の衝突を避けたい場合や、より分かりやすい名前で使用したい場合に有用である。
import { capitalize as capitalizeString } from './string-utils.js';
import { helper as stringHelper } from './string-utils.js';
console.log(capitalizeString("hello")); // "Hello"
console.log(stringHelper("WORLD")); // "world"
名前空間インポート
* as 構文を使用することで、モジュールの全エクスポートを1つのオブジェクトにまとめてインポートできる。
名前空間として利用することで、複数のエクスポートを整理して管理できる。
import * as MathUtils from './math.js';
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.subtract(5, 3)); // 2
console.log(MathUtils.PI); // 3.14159
副作用のみのインポート
インポートする値を指定せずに import 文を記述すると、モジュールのコードを実行するだけで何もインポートしない。
これは、ポリフィルや、グローバルに副作用を適用するモジュールで使用される。
// モジュールのコードを実行するだけで、何もインポートしない
import './polyfills.js';
import './global-styles.css';
再エクスポート
再エクスポートとは、他のモジュールからインポートした値を、そのまま別のモジュールとして公開する方法である。
基本構文
export ... from 構文を使用することにより、インポートした値を再エクスポートできる。
1度インポートしてから再エクスポートする場合と比較して、変数に束縛されないため簡潔に記述できる。
// 特定の名前付きエクスポートを再エクスポートする
export { add, subtract } from './math.js';
// エイリアスを付けて再エクスポートする
export { capitalize as cap } from './string-utils.js';
// モジュールの全エクスポートを再エクスポートする
export * from './math.js';
// デフォルトエクスポートを名前付きで再エクスポートする
export { default as User } from './User.js';
// デフォルトエクスポートをデフォルトのまま再エクスポートする
export { default } from './logger.js';
バレルファイル (index.js) パターン
複数のモジュールを1つのエントリポイントにまとめて公開するパターンをバレルファイルパターンと呼ぶ。
index.jsファイル (または index.tsファイル) に再エクスポートをまとめることにより、利用側のインポートパスを短くできる。
// components/index.js (バレルファイル)
export { Button } from './Button.js';
export { Modal } from './Modal.js';
export { TextInput } from './TextInput.js';
export { default as Icon } from './Icon.js';
// 利用側: バレルファイルを通じて複数のコンポーネントを1行でインポートできる
import { Button, Modal, TextInput } from './components';
// 個別パスを指定する場合と比べて記述が簡潔になる
// import { Button } from './components/Button.js';
// import { Modal } from './components/Modal.js';
// import { TextInput } from './components/TextInput.js';
バレルファイルを使用する時の注意点を以下に示す。
- Tree Shakingへの影響
- バンドラーによっては、バレルファイルを経由したインポートでTree Shakingが効きにくくなる場合がある。
- 特に大規模なライブラリでは、バレルファイルのインポートがバンドルサイズの増加につながる場合がある。
- 循環依存のリスク
- バレルファイルを多用すると、モジュール間の循環依存が発生しやすくなる。
動的インポート
動的にインポートとは、実行時に必要に応じてモジュールを非同期でロードする方法である。
import()の基本
import() は関数のような構文で呼び出す動的インポートである。
通常の import 文と異なり、コードの任意の場所に記述でき、条件分岐やイベントに応じてモジュールを遅延ロードできる。
import() はPromiseを返し、モジュールオブジェクトに解決される。
// Promiseチェーンを使用した動的インポート
import('./math.js')
.then(mathModule => {
console.log(mathModule.add(5, 3)); // 8
});
// async/awaitを使用した動的インポート
const math = await import('./math.js');
console.log(math.add(5, 3)); // 8
// デフォルトエクスポートにアクセスする場合は .default を使用する
const logModule = await import('./logger.js');
logModule.default("メッセージ");
使用例
動的インポートの代表的な使用場面を以下に示す。
- 条件に応じたモジュールの読み込み
// ユーザのロールに応じて異なるモジュールを読み込む async function loadUserModule(userRole) { if (userRole === 'admin') { const { AdminPanel } = await import('./admin-panel.js'); return new AdminPanel(); } else { const { UserDashboard } = await import('./user-dashboard.js'); return new UserDashboard(); } }
- ユーザの操作をトリガーとした遅延読み込み
// ボタンが押下された時にのみモジュールを読み込む button.addEventListener('click', async () => { const { initChart } = await import('./chart-library.js'); initChart(document.getElementById('chart-container')); });
- React.lazyを使用したコンポーネントの遅延読み込み
import React, { lazy, Suspense } from 'react'; // 動的インポートによる遅延読み込み const HeavyComponent = lazy(() => import('./HeavyComponent.js')); function App() { return ( <Suspense fallback={<div>読み込み中...</div>}> <HeavyComponent /> </Suspense> ); }
Import Attributes (ES2025)
Import Attributesは、モジュールの読み込み時にメタデータを指定する。
基本構文
Import Attributesは、import 文に with キーワードを付加して属性を指定する構文である。
以前の仕様ではImport Assertions (assert キーワード) と呼ばれていたが、ES2025でImport Attributes (with キーワード) に変更された。
// 静的インポートでの使用
import data from './data.json' with { type: 'json' };
import styles from './styles.css' with { type: 'css' };
// 動的インポートでの使用
const config = await import('./config.json', { with: { type: 'json' } });
JSONモジュールの読み込み
type: 'json' を指定することにより、JSONファイルをモジュールとして安全に読み込むことができる。
// JSONファイルをインポートする (type: 'json' を指定する)
import packageJson from './package.json' with { type: 'json' };
console.log(packageJson.name); // パッケージ名
console.log(packageJson.version); // バージョン番号
セキュリティ上のメリットとして、type: 'json' を明示指定されたリソースはJavaScriptコードとして実行されない。
悪意あるサーバがJSONを返すべき場所でJavaScriptを返した場合でも、コードが実行されることを防ぐことができる。
CSSモジュールの読み込み
type: 'css' を指定することで、CSSファイルをCSSStyleSheetオブジェクトとしてインポートできる。
これは、Web ComponentsのShadow DOMでスタイルを適用する時に活用できる。
// CSSファイルをモジュールとしてインポートする
import styles from './component.css' with { type: 'css' };
// Web ComponentsのShadow DOMにスタイルを適用する
class MyComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [styles];
}
}
Webブラウザの対応状況 および Node.jsサポートを以下に示す。
- Webブラウザ
- Chrome 121以降、Edge 121以降、Safari 17.4以降でサポートされている。
- Node.js
- v22以降で
with構文が標準サポートされている。
- v22以降で
CommonJSとの比較
下表に、JavaScriptのもう1つの主要なモジュールシステムであるCommonJS (CJS) とESモジュール (ESM) の違いを示す。
| 項目 | CommonJS | ESモジュール |
|---|---|---|
| 読み込み構文 | require('module') | import ... from 'module' |
| エクスポート構文 | module.exports / exports.name | export / export default |
| ロードタイミング | 同期・実行時 | 非同期・解析時 |
| 静的解析 | 困難 (動的) | 容易 (静的) |
| Tree Shaking | 困難 | 容易 |
| トップレベルawait | 不可 | 可能 |
| strictモード | 手動指定 ("use strict") | 自動適用 |
| ブラウザサポート | なし (バンドラーが必要) | ネイティブサポート |
| __dirname / __filename | 利用可能 | import.meta.url で代替 |
下表に、Node.jsでのESMサポートに関する設定を示す。
| 設定 | 説明 |
|---|---|
| .mjs拡張子 | ファイルをESMとして扱う。 |
| .cjs拡張子 | ファイルをCommonJSとして扱う。 |
package.jsonファイルへの "type": "module" の指定 |
ディレクトリ内の .js拡張子 のファイルをESMとして扱う。 |
| Node.js v22以降 | CommonJSモジュールからESMを require() で読み込めるようになった。
|
Node.jsにおける __dirname の代替方法を以下に示す。
// ESMでは __dirname が使用できないため、import.meta.url で代替する
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname); // 現在のファイルが存在するディレクトリのパス
関連情報