こんにちは!フロントエンドからNode.jsの深層まで、JavaScriptの生態系を隅々まで知り尽くしたチーフアーキテクトの私です。
今回は、Node.jsにおけるモジュールシステム、つまり 「CommonJS (`require`)」と「ESM (`import`)」の決定的な違い、そして変数のスコープがどう変化するか について、本質から分かりやすく紐解いていきましょう。
「JavaScriptを書き始めたけれど、`require` と `import` がごちゃ混ぜになってエラーが出る……」
「モジュール間で変数をどうやって共有しているのか、裏側の仕組みがモヤモヤする……」
そんな悩みを持っていませんか?大丈夫です。ここをクリアすれば、Node.jsのモジュールローダーが頭の中で完全にビジュアライズできるようになりますよ。一緒に一歩ずつ紐解いていきましょう!
—
そもそも「モジュール」ってなんだろう?
JavaScriptはもともと、1つの巨大なグローバル空間にすべてのスクリプトが読み込まれる、少しルーズな言語として生まれました。そのため、どこで変数を宣言しても同じ空間(グローバルスコープ)を汚染してしまう危険性があったのです。
そこで登場したのが「モジュールシステム」です。
これは、コードを機能ごとに小さなファイル(=モジュール)に分割し、必要な変数や関数だけを安全に外の世界へ公開(エクスポート)し、必要な場所で取り込む(インポート)ための仕組みです。
Node.jsでは、歴史的な背景から主に2つのシステムが共存しています。
1. CommonJS (`require` / `module.exports`):昔からNode.jsを支えてきた伝統的な仕組み
2. ESM (ECMAScript Modules / `import` / `export`):モダンなJavaScriptの標準規格
この2つ、書き方が違うだけでなく、変数のスコープの扱いやローダーの挙動において、裏側で全く異なるアプローチを取っています。ここが今回の最重要ポイントです。
—
1. CommonJS (`require`) の世界:変数はどう守られている?
まずは、長年Node.jsの主役だったCommonJSを見てみましょう。
基本の使い方
// math.js (モジュール側)
const secretValue = “秘密の定数”;
function add(a, b) {
return a + b;
}
// module.exportsを通じて外の世界へ公開する
module.exports = {
add,
};
// app.js (呼び出し側)
const math = require(‘./math.js’);
console.log(math.add(2, 3)); // 5 出力される
console.log(math.secretValue); // undefined!外からは見えない
裏側で何が起きているのか?(スコープの秘密)
「あれ? `math.js` で宣言した `secretValue` はどこに行ったの?」と思いましたよね。
実はNode.jsが `require()` を実行するとき、モジュールファイルを読み込んで、自動的に次のような「即時実行関数(IIFE)」のラッパーでコード全体を包み込んでいます。
// Node.jsのモジュールローダーが裏側で行っていることのイメージ
(function (exports, require, module, __filename, __dirname) {
// あなたが書いたコードはここにすっぽり包まれる!
const secretValue = “秘密の定数”;
function add(a, b) {
return a + b;
}
module.exports = { add };
});
この仕組みのおかげで、ファイル内で宣言した変数(`secretValue` など)は、その関数スコープの中に閉じ込められ、外のファイルから勝手に見られたり書き換えられたりすることが物理的に不可能になるのです。これがCommonJSの安全性の源泉です。
—
2. ESM (`import`) の世界:静的な束縛とスコープ
次に、現代の標準である ESM (`import` / `export`) を見てみましょう。ブラウザでもそのまま動く、モダンな書き方ですね。
基本の使い方
// math.js (モジュール側)
export const secretValue = “秘密の定数”; // exportで直接公開
export function add(a, b) {
return a + b;
}
// app.js (呼び出し側)
import { add, secretValue } from ‘./math.js’;
console.log(add(2, 3)); // 5
console.log(secretValue); // “秘密の定数” (名前を指定すればインポートできる)
CommonJSとの決定的な違い:「静的構造」と「ライブバインディング」
ESMがCommonJSと大きく異なるのは、コードを実行する前に(パースの段階で)モジュールの依存関係を完全に静的に解決するという点です。
さらに、ESMでインポートした変数は、単なるコピーではなく「ライブバインディング(Live Binding)」という特殊な状態で結ばれます。言葉だけだと難しいので、例を見てみましょう。
// counter.js
export let count = 0;
export function increment() {
count++;
}
// main.js
import { count, increment } from ‘./counter.js’;
console.log(count); // 0
increment();
console.log(count); // 1 (モジュール側の値が変化すると、インポート側の値も連動して変わる!)
このように、ESMでは変数の参照がガッチリと結びついています。ただし、インポートされた変数は「読み取り専用(Read-only)」であり、インポートした側のファイルから勝手に `count = 10` のように書き換えることはエラーになります。
—
3. 初学者が陥りがちな罠とエラー
Node.jsでモジュールを扱う際、初心者が必ずと言っていいほど踏む地雷がいくつかあります。ここで事前に回避策を学んでおきましょう。
罠1:CommonJSとESMの混在によるエラー
Node.jsでデフォルト設定のまま、ESMの `import` を使おうとして次のようなエラーに遭遇したことはありませんか?
> `SyntaxError: Cannot use import statement outside a module`
原因と対策:
Node.jsは、デフォルトでは `.js` ファイルを CommonJS として解釈します。ESMを使うには、以下のいずれかの対応が必要です。
1. ファイルの拡張子を `.mjs` にする。
2. プロジェクトのルートにある `package.json` に `”type”: “module”` を追記する。
{
“name”: “my-node-project”,
“version”: “1.0.0”,
“type”: “module”
}
罠2:CommonJSでの `require` と `module.exports` の勘違い
たまに以下のようなコードを書く人がいますが、これは意図通りに動きません。
// ❌ やりがちなミス
exports = { add }; // これだと module.exports との参照が切れてしまう!
`module.exports` と `exports` は最初は同じオブジェクトを指していますが、`exports = …` と直接代入してしまうと、Node.jsがエクスポート用として監視している `module.exports` への参照が外れてしまいます。必ず `module.exports = …` または `exports.add = …` のようにプロパティを生やす形で記述しましょう。
—
まとめ:モジュールシステムを制する者はNode.jsを制す
今回は、CommonJSとESMのスコープの違いや、裏側のローダーの仕組みについて深く掘り下げてみました。
- CommonJS (`require`):関数スコープでファイルを包み込み、変数を完全にカプセル化する。動的な読み込みが可能。
- ESM (`import`):静的解析を前提とし、ライブバインディングによってモジュール間で変数を安全かつリアルタイムに共有する。
それぞれの仕組みが「なぜその仕様になっているのか」という設計思想まで理解できると、エラーに直面したときも慌てず冷静に対処できるようになります。
ここをクリアできれば、JavaScriptのモジュール周りの挙動はもうバッチリマスターできていますよ!自信を持って次のステップへ進んでください。それでは、快適なコーディングライフを!