JavaScript基礎 #7 モジュール — importとexport

基礎シリーズの最終回です。これまで扱ってきた道具でコードを書いていると、1つのファイルがすぐに長くなります。今回の記事では、複数のファイルに分けるための道具 — ES Modules を整理します。

モジュールはなぜ必要なのですか? #

小さなスクリプトなら1つのファイルで足りますが、実際のプロジェクトでは:

  • 関数が100個も集まっている1つのファイルは読みにくい
  • 同じ変数名が別の箇所で使われていて衝突する
  • ある関数がどこで定義されたか追跡するのが大変

これを解決するため、JavaScriptは ファイル単位でコードを分け 、必要なものだけを取ってきて使うシステム — モジュール を備えています。

Named Export — 名前をそのまま #

最もシンプルな形から。

src/math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export const PI = 3.14159;

export キーワードを関数 / 変数の前に付けると、それがそのままモジュールの外に公開されます。取ってきて使う側:

src/main.js
import { add, subtract, PI } from './math.js';

console.log(add(2, 3));        // 5
console.log(subtract(5, 2));   // 3
console.log(PI);                // 3.14159

import { ... } の中括弧が肝心です。モジュールが 公開している名前の中から必要なものだけを 選んで取ってきます。存在しない名前を取ろうとするとエラーになります。

まとめてexport #

宣言する箇所ごとに export を付ける代わりに、末尾でまとめて書くこともできます。

末尾でまとめて
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

export { add, subtract };

機能は同じです。チームのコンベンションに従って、どちらかに統一して使えば大丈夫です。

名前を変える — as #

名前を変える
// export 時
export { add as plus, subtract as minus };

// import 時
import { add as sum } from './math.js';

ライブラリ内部の名前と、外部に公開する名前を変えたいとき、または別のモジュールから同じ名前が来て衝突したときに使います。

Default Export — モジュールの代表 #

モジュール1つに 1つだけ 置けるexportです。「このモジュールを代表する値」のような意味です。

src/Logger.js
export default class Logger {
  log(message) {
    console.log(`[LOG] ${message}`);
  }
}
取り込み
import Logger from './Logger.js';
// 中括弧なし、名前は自由

const logger = new Logger();
logger.log('hello');

import 名前 の名前は、呼び出す側が自由に 決められます。同じdefaultを、あるファイルでは Logger 、別のファイルでは MyLogger として受け取っても大丈夫です。

default は本当に必要ですか? #

Reactのようなライブラリが import React from 'react' のようにdefaultを使っているので必須に見えますが、default よりnamedのほうが良い というのが最近のコンベンションです。理由は:

  1. 自動補完がより正確 — モジュールにどんな名前があるかをIDEが把握しやすい
  2. リファクタリングが安全 — 名前を変えると他のファイルにも自動で反映される
  3. 呼び出す側が名前を自由に決められると、コードの一貫性が崩れる

ほとんどのコードは named export で十分です。default は、コンポーネント(React)、またはライブラリの単一インスタンス(createLogger() の結果など)でだけ使うのが良いです。

Re-export — 1か所にまとめる #

複数のファイルを、1つのエントリーポイントからまとめてエクスポートしたいとき。

src/utils/index.js
export { add, subtract } from './math.js';
export { capitalize, slugify } from './string.js';
export { default as Logger } from './Logger.js';
取り込む側
import { add, capitalize, Logger } from './utils';
// './utils/index.js' が自動的に解決される

フォルダの中の複数のファイルを、1つのモジュールのように公開するパターンです。ライブラリ群がよく採用する構造で — npm に上がっているパッケージのエントリーポイントファイルが、まさにこの形です。

Side-Effect Import — 実行のみ #

値を取らずに、モジュールを 実行だけ させたいとき。

実行のみ
import './styles.css';        // CSS 適用
import './polyfills.js';      // グローバル環境セットアップ

CSS、ポリフィル、またはモジュールが自分の中で自動的に何かを実行する(例: グローバルオブジェクトに登録する)場面に向いています。実務ではCSSが最もよくあります。

Dynamic Import — 実行時に呼び出す #

import キーワードを関数のように使うと、非同期で モジュールを取得できます。

dynamic import
async function loadHeavy() {
  const module = await import('./heavy.js');
  module.run();
}

button.addEventListener('click', loadHeavy);

戻り値は Promise です。モジュールが持つ export がすべてオブジェクトのプロパティとして入っています。

このパターンは、ユーザーがその機能を使うときだけコードをダウンロードする コードスプリッティングの基盤になります。Next.jsの dynamic()、Reactの lazy() は、すべてこれをラップしたものです。

* import — 全体を名前空間として #

全体 import
import * as math from './math.js';

math.add(2, 3);     // 5
math.PI;            // 3.14159

モジュールのすべてのexportを、math オブジェクトのプロパティとして受け取ります。何があるか正確に分からないとき、または名前空間のようにまとめて使いたいときに向いています。一般的な場面では、{ add, PI } のようにピンポイントで取ってくるほうがすっきりします。

ブラウザでモジュールを使う #

HTMLでモジュールスクリプトをロードするには、type="module" 属性が必須です。

index.html
<script type="module" src="./main.js"></script>

type="module" があれば:

  • import/export が動作する
  • 自動で defer のように動作する(DOM構築後に実行)
  • 変数がグローバルではなく モジュールスコープ に隔離される

この属性なしで <script src="..."> を使うと、古いモードになり import が動作しません。Viteのようなツールはうまく処理してくれます。

.js 拡張子 — 付ける必要がありますか? #

ここはJavaScriptのエコシステムでとても混乱する部分です。

  • 標準のES Modules(ブラウザ、Node): 拡張子の 明示が必須import './math.js'
  • バンドラを通すコード(Vite、webpack、Next.js): 通常は拡張子を 省略可能import './math'

このシリーズでは、モダンな流れに従って 拡張子を明示 します。Nodeとブラウザの標準の両方で動作するからです。ただしReact / Next.jsのような環境のコードでは、拡張子を省略しているのをよく見ることになります。

CommonJS — Nodeの古いモジュールシステム #

NodeがESMの標準化前に独自に作ったモジュールシステムです。

CommonJS — 古い Node スタイル
// エクスポート
function add(a, b) {
  return a + b;
}
module.exports = { add };

// 取り込み
const { add } = require('./math');

require / module.exports という形が見えれば、CommonJSです。古いパッケージや古い資料でよく出会います。新しいコードはほぼESM(import/export)で書きますが、互換性の問題で2つのシステムが混在して動いている場面はまだ多いです。

ほとんどのモダンツール(Vite、Next.js、Bun)は、2つのシステムを自動で扱ってくれるので、入門段階ではあまり深く気にする必要はありません。

まとめ #

今回の記事で整理した内容:

  • ESM(ES Modules)がJavaScriptの標準モジュールシステム
  • exportimport { ... } — named export が基本
  • export default はモジュールに1つだけ、呼び出す側が名前を自由に決める
  • named export は default より自動補完 / リファクタリングに有利
  • Re-export でフォルダ内の複数ファイルを1つのエントリーポイントにまとめる
  • Dynamic import で非同期ロード(コードスプリッティングの基盤)
  • <script type="module"> がブラウザでESMを有効にする
  • CommonJS(require/module.exports)は Node の古いシステム

基礎シリーズを終えて #

7編を通じて私たちが扱った内容:

  1. はじめてとセットアップ — 環境を整える (#1)
  2. 変数と型let/const、8つの型、プリミティブ vs 参照 (#2)
  3. 制御フロー — if/for/switch、for...of (#3)
  4. 関数 — 宣言/関数式/アロー、仮引数、ホイスティング (#4)
  5. オブジェクトと配列 — map/filter/reduce、spread、分割代入 (#5)
  6. 文字列とテンプレートリテラル — メソッド、正規表現の基礎 (#6)
  7. モジュールimport/export (今回の記事)

ここまで身につければ、JavaScriptで小さなツールやスクリプトを自信を持って書けるようになります。次の 中級シリーズ では、クラス、非同期(Promise/async/await)、イテレータ、オプショナルチェイニング、fetch — モダンJavaScriptの本当の表現力を引き上げる道具を扱います。

X