【入門編】【中級者向け】Node.jsのモジュールスコープ:requireとimportで変数の見え方はどう変わるか – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは! Node.jsやモダンなフロントエンド開発の現場へようこそ。

JavaScriptを書き始めてしばらく経つと、「あれ、このファイルで宣言した変数、どこからでもアクセスできるわけじゃないんだな?」と気づく瞬間がやってきますよね。特にNode.js環境で開発をしていると、古い書き方である `require`(CommonJS)と、モダンな標準規格である `import`(ESM)のどちらを使うかによって、変数の見え方が微妙に変わり、頭を悩ませることがあるかもしれません。

ここをクリアすれば、Node.jsのモジュールシステムと変数スコープの関係はバッチリマスターできますよ。今日は、V8エンジンの裏側の仕組みにも少しだけ触れながら、CommonJSとESMにおける変数の隔離メカニズムを優しく、かつ深く紐解いていきましょう。

—

1. モジュールスコープってそもそも何だろう?

ブラウザの古いJavaScriptでは、すべてのスクリプトがグローバルスコープ(`window`オブジェクト)を共有していました。そのため、どこで変数を宣言しても名前が衝突してしまう「名前空間汚染」という大問題が起きていたんです。

それを解決するためにNode.jsなどの環境では、「1つのファイル=1つの独立した世界(モジュール)」という仕組みを採用しました。これがモジュールスコープです。

[ app.js ] ── (独立した世界) ── 外からは見えない!
[ utils.js ] ── (独立した世界) ── 外からは見えない!

ファイル内で `const x = 10;` と書いても、それはそのファイルの中だけの秘密。デフォルトでは、他のファイルからその変数を勝手に見ることはできません。

では、この「外の世界とのデータのやり取りと変数の隠蔽」を、`require` と `import` はそれぞれどう実現しているのでしょうか?

—

2. CommonJS(require)の世界:関数で包み込む仕組み

まずは、Node.jsの歴史を支えてきた CommonJS(`require` と `module.exports`)から見ていきましょう。

実は、Node.jsがCommonJSのファイルを読み込むとき、V8エンジンはあなたの書いたコードを勝手に秘密の関数(ラッパー関数)で包み込んで実行しています。イメージとしては、こんな感じです。

// Node.jsが内部でラップしている擬似コード
function (exports, require, module, __filename, __dirname) {
// あなたが書いたコードは、この関数のスコープの中に配置される!
const secretVariable = “秘密のデータ”;

exports.getData = function () {
return secretVariable;
};
}

この仕組みのおかげで、`secretVariable` はこの関数のローカル変数となり、グローバル空間を汚染せずに済みます。他のファイルからこのファイル内の変数に直接アクセスすることは、物理的に不可能なのです。

コード例:CommonJSでの変数共有

// counter.js (CommonJS)
let count = 0; // このファイルだけのローカル変数

function increment() {
count++;
return count;
}

// 外に公開したいものだけを module.exports に詰める
module.exports = {
increment,
// 注意: countそのものではなく、現在の「値」や操作する関数を渡す
getCount: () => count
};

// main.js
const counter = require(‘./counter.js’);

console.log(counter.increment()); // 出力: 1
console.log(counter.increment()); // 出力: 2

// counter.js の中の変数 `count` に直接アクセスしようとすると…?
// console.log(counter.count); // 出力: undefined (直接見えない!)

CommonJSでは、変数を外に見せたい場合は必ず `module.exports` を介す必要があります。変数のカプセル化(隠蔽)が非常に綺麗に保たれる理由がここにあります。

—

3. ESM(import / export)の世界:静的構造とライブバインディング

次に、現代のJavaScript標準である ESM(ECMAScript Modules)を見てみましょう。拡張子を `.mjs` にしたり、`package.json` に `”type”: “module”` を記述することで有効になります。

ESMは、CommonJSの「あとから動的に読み込む」アプローチとは異なり、コードを実行する前にファイル間の依存関係をすべて解析する「静的構造」を持っています。

変数の見え方の最大の違い:「ライブバインディング」

ESMがCommonJSと決定的に違うのは、エクスポートされた変数が「ライブバインディング(Live Binding)」という性質を持つ点です。言葉だけ聞くと難しそうですが、要するに「エクスポート元の変数が書き換わると、インポート側の変数(厳密には参照)も連動してリアルタイムに変化する」ということです。

百聞は一見に如かず、コードで確認してみましょう。

// state.mjs (ESM)
export let score = 100;

export function updateScore(newScore) {
score = newScore; // 変数の値を書き換える
}

// main.mjs
import { score, updateScore } from ‘./state.mjs’;

console.log(score); // 出力: 100

updateScore(500);

// なんと、インポート側の score の値も自動的に更新される!
console.log(score); // 出力: 500

おっと、ここで少し混乱しましたか? 「あれ? 他のファイルの変数を直接書き換えられるなんて、スコープの隔離として危なくない?」と感じたかもしれません。

ですが安心してください。ESMにおいて、インポートした側(`main.mjs`)から `score = 300;` のように直接値を代入し直そうとすると、JavaScriptエンジンが SyntaxError(構文エラー)を吐いて強制終了させます。

// main.mjs
import { score } from ‘./state.mjs’;

score = 300; // ❌
// エラー: Assignment to constant variable.
// (ESMでインポートした変数は、インポート側からは読み取り専用の定数扱いになるため)

つまり、「エクスポートした側は値を自由に変えられるが、インポートした側は読み取り専用で、かつ値の変更をリアルタイムにライブ受け取りできる」というのがESMの変数スコープの挙動なのです。

—

4. 陥りがちな罠とデバッグのポイント

Node.jsでモジュールを扱う際、初学者がよくハマるポイントを2つ整理しておきましょう。

罠1: CommonJSでプリミティブ値をそのままエクスポートして混乱する

CommonJSで以下のような書き方をすると、データの同期ズレが起きてバグの温床になります。

// 良くない例 (counter.js)
let count = 10;
module.exports = count; // プリミティブ値(数値)をそのまま渡す

// main.js
const currentCount = require(‘./counter.js’);
// currentCount はただの「10という数値のコピー」を受け取るだけ。
// 元の counter.js 内で count が変わっても、main.js 側の値は連動しません!

解決策: 状態を共有したい場合は、必ずオブジェクトや関数経由でやり取りしましょう。

罠2: CommonJSとESMの混在によるスコープの勘違い

Node.js環境で `require()` を使ってESMのモジュールを読み込もうとすると、エラーが発生することがあります(逆も然り)。モジュールシステムが混ざると、変数のスコープだけでなく、非同期読み込みの挙動そのものが変わってしまうため、プロジェクト全体でどちらに統一するかを明確にしておくことが、トラブルを防ぐ最大の近道です。

—

まとめ

今回の重要なポイントをギュッと凝縮して振り返りましょう。

1. モジュールスコープの基本:
Node.jsではファイルごとに独立したスコープがあり、変数はデフォルトでファイルの外から見えない(カプセル化されている)。
2. CommonJS(`require`):
コードが関数でラップされることでスコープが守られる。エクスポートするのは「値やオブジェクトのコピー(参照)」であり、静的なライブ連動はしない。
3. ESM(`import`):
静的解析ベース。エクスポートされた変数はライブバインディングにより、元ファイルの変化がインポート側にもリアルタイムに反映される(ただしインポート側からは書き換え不可)。

この仕組みを頭の片隅に置いておけば、変数が見えなくてバグったときも、「あ、いまCommonJSのコピー値を受け取っているからだな」「ESMのライブバインディングだから値が連動しているんだな」と、V8エンジンの挙動を脳内で正確にトレースできるようになります。

ここをクリアできれば、もうNode.jsの変数管理で迷うことはありません。自信を持って、次のステップへ進んでくださいね!

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