JavaScript/TypeScript

モジュールでコードを分ける

4
  • JavaScript

ファイルが増えると、関数や定数の共有が必要になります。ES モジュール(ESM)import / export で依存を明示します。ここでは 文法とスコープ に絞ります。Node の "type": "module" やブラウザの <script type="module"> は、それぞれ実行環境側の話です。

参考(MDN)


目次

  1. なぜモジュールを使うか
  2. 名前付き export / import
  3. デフォルト export / import
  4. 再エクスポートと import まとめ
  5. モジュールのスコープ
  6. うまくいかないとき

なぜモジュールを使うか

モジュールを使うと次のようなメリットがあります。

  • 関心の分離 — 1 ファイル 1 責務にしやすい
  • 名前の衝突回避 — ファイルスコープで変数が閉じる
  • 依存の明示 — 先頭の import で必要なものが読める
  • 再利用 — ユーティリティをプロジェクト横断で共有

従来の「HTML に <script> を並べる」方式では、グローバル変数が増えて衝突しやすくなります。ESM では 各ファイルが独立したモジュールスコープ を持ちます。

// utils/math.jsexport function add(a, b) {  return a + b;}// main.jsimport { add } from "./utils/math.js";console.log(add(2, 3)); // 5

名前付き export / import

名前付き export は、複数の識別子を公開するときに使います。import 側は 同じ名前(または as でリネーム)が必要です。

宣言と同時に export

// strings.jsexport const APP_NAME = "MyApp";export function capitalize(str) {  return str.charAt(0).toUpperCase() + str.slice(1);}export class Formatter {  static upper(s) {    return s.toUpperCase();  }}

あとからまとめて export

const PI = 3.14159;function area(r) {  return PI * r * r;}export { PI, area };

import

import { APP_NAME, capitalize } from "./strings.js";import { capitalize as cap } from "./strings.js";console.log(cap("hello")); // "Hello"import { APP_NAME, capitalize, Formatter } from "./strings.js";

デフォルト export / import

デフォルト export は、モジュールあたり 1 つ(実務上は 1 メインの値)を公開するときに使います。import 側は 任意の名前 を付けられます。

// logger.jsexport default function createLogger(prefix) {  return {    info(msg) {      console.log(`[${prefix}] ${msg}`);    },  };}
import createLogger from "./logger.js";const log = createLogger("APP");log.info("起動");

名前付きとデフォルトの併用

// http.jsexport const BASE_URL = "/api";export default async function fetchJSON(path) {  const res = await fetch(`${BASE_URL}${path}`);  return res.json();}
import fetchJSON, { BASE_URL } from "./http.js";

使い分けの目安

名前付き デフォルト
複数の関数・定数を公開 モジュールの「主役」が 1 つ
名前で検索・リファクタしやすい import 側で任意の名前を付けられる
ツリーシェイクと相性がよい import 名がファイルごとにバラけやすい

ユーティリティでは 名前付きを基本 にし、デフォルトは「このファイルの主役が 1 つだけ」のときに使います。


再エクスポートと import まとめ

再エクスポート(barrel)

// utils/index.jsexport { add, subtract } from "./math.js";export { capitalize } from "./strings.js";
import { add, capitalize } from "./utils/index.js";

小規模プロジェクトでは便利ですが、巨大な barrel は循環依存の原因になりやすいので、公開 API だけに絞るのが安全です。

import * as 名前空間

import * as math from "./math.js";console.log(math.add(1, 2));

動的 import()

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

必要になったタイミングでだけモジュールを読み込む コード分割 に使います。静的 import はファイル先頭で依存を固定するのに対し、動的 import は条件分岐の中に書けます。


モジュールのスコープ

  • モジュール内の const / let / functionモジュールスコープ(グローバルに漏れない)
  • モジュールは strict モード相当 で実行される
  • トップレベルの await が使える(モジュールが async コンテキスト)
  • importホイスティング されるが、実行前に依存グラフが解決される(循環依存は注意)
// counter.jslet count = 0;export function increment() {  count += 1;  return count;}export function getCount() {  return count;}

モジュール内の count は外部から直接触れず、export した関数だけが窓口になります。


うまくいかないとき

デフォルトと名前付きの取り違え

// logger.jsexport default function log() {}// NG// import { log } from "./logger.js";// OKimport log from "./logger.js";

逆に名前付きをデフォルト import しようとしても undefined になります。

循環依存で undefined が入る

a.jsb.js を import し、b.jsa.js を import すると、初期化順の関係で まだ export されていない値 を読むことがあります。

// a.jsimport { b } from "./b.js";export const a = "A";// b.jsimport { a } from "./a.js";export const b = a + "B"; // タイミングによっては a が undefined

解決策は依存の向きを整理する、共通部分を第 3 ファイルに切り出す、関数内で遅延参照する、などです。

シェア