【JavaScript】モジュール(import/export)の基本

javascript-icon JavaScript

こんにちは、かつコーチです。

前回はasync/awaitを使った非同期処理の書き方を解説しました。

今回のテーマは、コードを複数のファイルに分割して管理するための仕組み「モジュール(import/export)」です。

これまでの記事のコードは、すべて1つのファイルの中で完結する前提で書いてきました。

しかし実際の開発では、機能ごとにファイルを分け、必要な部分だけを読み込んで使うのが基本です。

今日はその分割・読み込みの仕組みを一緒に学んでいきましょう。

モジュールとは?

「ファイルを部品として扱う」仕組み

モジュールとは、1つの機能・役割ごとにコードをまとめたファイルのことです。

そして、モジュール同士をつなぐための構文がexport(外部に公開する)とimport(他のファイルから読み込む)です。

// math.js(モジュール側)
export function add(a, b) {
  return a + b;
}
// main.js(モジュールを使う側)
import { add } from "./math.js";

console.log(add(1, 2)); // 3

math.jsexportしたadd関数を、main.js側でimportして使っています。

「必要な部品(関数や変数)だけを、必要なファイルから取り出して使う」——これがモジュールの基本的な考え方です。

なぜモジュールが必要なのか

すべてのコードを1つのファイルに書き続けると、次のような問題が起こります。

  • ファイルが数千行に膨れ上がり、目的のコードを探すのが大変になる
  • 変数名や関数名がすべて同じ空間(グローバルスコープ)に存在するため、名前の重複(衝突)が起きやすい
  • 「この関数はどこで使われているか」を追いにくくなる

モジュールを使うと、機能ごとにファイルを分けられるだけでなく、それぞれのファイルが独立したスコープを持つため、名前の衝突を気にせずコードを書けるようになります。

私が最初にモジュールを学んだときは、「関数を切り出すだけで何が嬉しいんだろう」と正直ピンときていませんでした。

しかし、複数人でコードを書くようになってから、「同じutils.jsファイルに全員が違う関数を書き足していって収拾がつかなくなる」という状況を経験し、モジュール分割のありがたみを痛感しました。

exportの書き方

名前付きexport

exportをつけると、その関数・変数・クラスを他のファイルから読み込めるようになります。

// utils.js
export const TAX_RATE = 0.1;

export function calculateTax(price) {
  return Math.floor(price * TAX_RATE);
}

export function formatPrice(price) {
  return `¥${price.toLocaleString()}`;
}

このように複数のものをexportする書き方を名前付きexportと呼びます。

読み込む側は、{ }の中に欲しいものを名前で指定します。

// main.js
import { TAX_RATE, calculateTax, formatPrice } from "./utils.js";

const price = 1000;
const tax = calculateTax(price);

console.log(formatPrice(price + tax)); // ¥1,100

必要なものだけを選んで読み込めるのが、名前付きexportの利点です。

デフォルトexport

1つのファイルにつき1つだけ、「このファイルの代表」としてexport defaultをつける書き方もあります。

// User.js
export default class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    console.log(`こんにちは、${this.name}です`);
  }
}
// main.js
import User from "./User.js"; // { }をつけずに読み込む

const user = new User("かつコーチ");
user.greet(); // こんにちは、かつコーチです

デフォルトexportはimportするときに{ }をつけず、好きな名前をつけて読み込める点が名前付きexportと異なります。

「クラスや関数を1つだけ、そのファイルの主役として公開したいとき」にデフォルトexportを使うのが一般的です。

importの書き方バリエーション

名前を変更して読み込む

import時にasをつけると、読み込む側で名前を変更できます。

import { calculateTax as tax } from "./utils.js";

console.log(tax(1000)); // 100

すでに同じ名前の変数・関数がファイル内にある場合など、名前の衝突を避けたいときに使います。

すべてをまとめて読み込む

* asを使うと、そのファイルのexportをすべてまとめて1つのオブジェクトとして読み込めます。

import * as utils from "./utils.js";

console.log(utils.TAX_RATE); // 0.1
console.log(utils.calculateTax(1000)); // 100

「このファイルの機能を、まとめてutils.という名前空間の下に置いておきたい」ときに便利な書き方です。

つまずきやすいポイント:名前付きexportとデフォルトexportの混同

実際につまずいた「{ }のつけ忘れ」問題

私が実務で最初にモジュールを使ったとき、名前付きexportとデフォルトexportの違いを理解しないままimportを書いてしまい、undefinedになって原因を探すのに時間がかかったことがあります。

❌ Before:exportの種類とimportの書き方が食い違っている

// utils.js(名前付きexportで公開している)
export function calculateTax(price) {
  return Math.floor(price * 0.1);
}
// main.js
import calculateTax from "./utils.js"; // { }をつけずに読み込んでしまった

console.log(calculateTax(1000)); // エラー:calculateTax is not a function

utils.js側は名前付きexportで公開しているのに、main.js側はデフォルトexport用の書き方({ }なし)で読み込んでしまっています。

このときcalculateTaxには、実際には「そのモジュールのデフォルトexport」が入ろうとする(存在しないのでundefinedになる)ため、関数として呼び出そうとしてエラーになります。

私は最初、エラーメッセージだけを見て「関数の中身がおかしいのかな」と処理内容を疑ってしまい、原因がimportの書き方にあると気づくまでにかなり時間を使いました。

✅ After:exportの種類に合わせてimportの書き方を揃える

// utils.js(名前付きexportのまま)
export function calculateTax(price) {
  return Math.floor(price * 0.1);
}
// main.js
import { calculateTax } from "./utils.js"; // 名前付きexportなので{ }をつける

console.log(calculateTax(1000)); // 100

exportdefaultがついているかどうか」で、読み込む側の書き方({ }の有無)が変わる、という対応関係を必ずセットで覚えておきましょう。

エラーが出たときは、まず読み込み元のファイルに戻って「これは名前付きexportか、デフォルトexportか」を確認する癖をつけると、原因の切り分けが早くなります。

循環参照によるつまずき

もう1つよくあるつまずきが、2つのファイルが互いをimportし合う「循環参照」です。

// a.js
import { b } from "./b.js";
export const a = "A" + b;
// b.js
import { a } from "./a.js";
export const b = "B" + a; // aがまだ読み込み中で正しく取得できないことがある

a.jsb.jsが互いを読み込み合っていると、どちらかが先に評価される際にもう一方の値がまだ確定しておらず、undefinedになることがあります。

このパターンに遭遇したら、「共通の値だけを別ファイル(shared.jsなど)に切り出す」ことで循環を断ち切るのが定石です。

応用:モジュールの実行環境による違い

ブラウザで使う場合

ブラウザでモジュールを使うときは、<script>タグにtype="module"を指定します。

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

type="module"をつけたスクリプトは自動的に厳格モード("use strict")で実行されるため、以前の記事で扱ったthisの挙動にも影響します(トップレベルのthisundefinedになるなど)。

Node.jsで使う場合

Node.jsでは、package.json"type": "module"を追加するか、拡張子を.mjsにすることでimport/export構文を使えるようになります。

{
  "type": "module"
}

指定がない場合、Node.jsは従来のrequire/module.exportsという別の書き方(CommonJS)をデフォルトとして扱います。

「今書いているファイルがどちらの方式なのか」を意識しておかないと、import構文がエラーになることがあるので注意しましょう。

まとめ

この記事のポイント

  • モジュールとはコードを機能ごとに分けたファイルで、exportで公開しimportで読み込む
  • 名前付きexportは{ }をつけて読み込み、デフォルトexportは{ }なしで好きな名前をつけて読み込む
  • exportの種類とimportの書き方が食い違うとundefinedになりエラーの原因になる
  • 2つのファイルが互いを読み込み合う循環参照には注意する
  • ブラウザはtype="module"、Node.jsはpackage.jsonの設定でモジュール構文を有効にする

次に読むべき記事

非同期処理とモジュール分割の基本が身についたところで、次はJavaScriptがHTMLと連携する仕組みである「DOM」を学んでいきましょう。

→ 次の記事:DOMとは?HTMLとJavaScriptをつなぐ仕組み

タイトルとURLをコピーしました