モジュールは一度だけ評価される

ES Modules では、同じモジュールが複数の場所からインポートされても、そのコードは一度だけ実行されます。これは「モジュールの評価は一度きり」という重要な特性です。

基本的な動作

同じモジュールを複数回インポートしても、モジュール内のコードは最初の1回しか実行されません。

// counter.js
console.log('counter.js が読み込まれました');

let count = 0;

export function increment() {
  count++;
}

export function getCount() {
  return count;
}
// a.js
import { increment } from './counter.js';
increment();
// b.js
import { getCount } from './counter.js';
console.log(getCount()); // 1
// main.js
import './a.js';
import './b.js';
import { getCount } from './counter.js';

// "counter.js が読み込まれました" は1回だけ出力される
console.log(getCount()); // 1

シングルトンパターンの実現

この特性により、モジュール単位でシングルトン(単一のインスタンス)が自然に実現できます。

// store.js
console.log('Store initialized');

const state = {
  user: null,
  items: []
};

export function setState(key, value) {
  state[key] = value;
}

export function getState(key) {
  return state[key];
}

どこからインポートしても、同じ state オブジェクトを共有します。

// userModule.js
import { setState } from './store.js';
setState('user', { name: '田中' });
// itemModule.js
import { getState } from './store.js';
console.log(getState('user')); // { name: '田中' }

モジュールのキャッシュ

モジュールは一度評価されると、結果がキャッシュされます。

初回インポート時

モジュールを読み込み、コードを実行し、エクスポートをキャッシュする

2回目以降のインポート

キャッシュからエクスポートを取得する(コードは再実行されない)

初期化コードの扱い

モジュールの初期化コードは1回だけ実行されることを利用して、セットアップ処理を行えます。

// config.js
import { readFileSync } from 'fs';

// この処理は1回だけ実行される
console.log('設定ファイルを読み込み中...');
const configData = readFileSync('./config.json', 'utf-8');
export const config = JSON.parse(configData);

注意点:実行順序

モジュールの評価順序は、依存関係の順に決まります。

// main.js
console.log('main start');
import './a.js';
import './b.js';
console.log('main end');
// a.js
console.log('a.js');
import './c.js';
// b.js
console.log('b.js');
import './c.js';
// c.js
console.log('c.js');

出力順序は以下のようになります。

c.js(a.js の依存として最初に評価)
a.js
b.js(c.js は既に評価済みなので再実行されない)
main start
main end

動的インポートでの挙動

動的インポート import() でも同様に、同じモジュールは一度しか評価されません。

// 何度呼び出しても、counter.js のコードは1回だけ実行される
const module1 = await import('./counter.js');
const module2 = await import('./counter.js');

console.log(module1 === module2); // true(同じモジュールオブジェクト)

この「一度だけ評価される」という特性を理解しておくことで、モジュール間での状態共有や初期化処理を効果的に設計できます。