JavaScriptの基礎 - ESモジュール

提供: MochiuWiki : SUSE, EC, PCB

概要

ESモジュール (ECMAScript Modules, ESM) は、ES2015 (ES6) で導入されたJavaScriptの標準モジュールシステムである。
各ファイルが独立したスコープを持ち、export で値を公開し、import で他のモジュールの値を利用する仕組みを提供する。

ESモジュールの主な特性を以下に示す。

  • strictモードの自動適用
    モジュールファイルは自動的にstrictモードで実行される。"use 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モジュールが持つ主要な特性を示す。

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
     };
    


名前付きエクスポート と デフォルトエクスポートの比較

下表に、名前付きエクスポートとデフォルトエクスポートの違いを示す。

名前付きエクスポート vs デフォルトエクスポート
項目 名前付きエクスポート デフォルトエクスポート
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 構文が標準サポートされている。



CommonJSとの比較

下表に、JavaScriptのもう1つの主要なモジュールシステムであるCommonJS (CJS) とESモジュール (ESM) の違いを示す。

CommonJS と ESモジュール
項目 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サポートに関する設定を示す。

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);  // 現在のファイルが存在するディレクトリのパス



関連情報