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

こんにちは!フロントエンドからNode.jsの深層まで、日夜JavaScriptと向き合っているシニアアーキテクトです。

今回は、Node.jsにおける「モジュールスコープ:CommonJS(require)とESM(import)で変数の見え方はどう変わるか」というテーマについて、トコトン深掘りしていきましょう。

「変数を作ったら他のファイルからも見えちゃった…」「`require`だと動くのに、`import`に変えたらエラーが出る…」そんなモヤモヤを抱えていませんか?ここをクリアすれば、JavaScriptのスコープとNode.jsのモジュールシステムの基本はバッチリマスターできますよ。

一見するとただの「ファイルの読み込み方の違い」に見えますが、実はV8エンジンのメモリ空間や、Node.jsがファイルを読み込むランタイムの仕組みにおいて、両者は全く異なるアプローチを取っています。

さあ、一緒にその内部の扉を開けてみましょう!

—

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

JavaScriptの世界では、かつて「すべての変数がグローバル空間(windowやglobal)を汚染してしまう」という暗黒の時代がありました。どこで書き換えられるか分からない変数は、バグの温床です。

そこで生まれたのが「モジュール」という概念です。
Node.jsでは、「1つのJavaScriptファイル=1つの独立したモジュール」として扱われます。

基本原則として、あるファイル内で宣言した変数(`let`, `const`, `var`)は、デフォルトではそのファイルの外からは一切見えません(カプセル化)。

// secret.js
const secretCode = “秘密のパスワード”;
// このファイルの外からは secretCode にアクセスできません!

この「ファイルの外から隠す」という安全な仕組みを、Node.jsは長年CommonJS(`require`)で支えてきました。そして近年、モダンな標準規格であるESM(`import` / `export`)が主役に躍り出ています。

この2つで、変数の見え方や扱われ方がどう違うのか、順を見ていきましょう。

—

2. CommonJS(`require`)の裏側と変数の見え方

まずは、長年Node.jsを支えてきた大御所、CommonJSの世界です。

実際に書いてみましょう

// counter.js (CommonJS)
let count = 0;

function increment() {
count++;
}

// 外に見せたいものだけを module.exports に詰める
module.exports = {
count,
increment
};

これを別のファイルで読み込んでみます。

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

console.log(counterModule.count); // 0 出力
counterModule.increment();
console.log(counterModule.count); // あれ?いくつになると思いますか?

ここで重要な「値のコピー」の罠

最後の `console.log`、いくつになったか予想がつきましたか?
答えは `0` のままです。

「えっ、`increment()`で `count++` したのに!?」と驚くかもしれません。
これは、CommonJSの `module.exports` が「値のコピー(スナップショット)」を渡しているからです。

Node.jsが `require` を実行するとき、内部的にはファイル全体を次のような即時実行関数(IIFE)でラップして実行しています。

// Node.jsが内部で行っているラップのイメージ
(function (exports, require, module, __filename, __dirname) {
let count = 0;
// … あなたが書いたコード …
module.exports = { count, increment };
});

この関数スコープの中に閉じ込められた `count` 変数は、外から直接書き換えることができません。`module.exports` に渡した瞬間、当時の値(`0`)がオブジェクトとして切り出されてしまうため、元のモジュール内の変数が変化しても、外側の世界にはリアルタイムで同期しないのです。

—

3. ESM(`import`)の裏側と「ライブバインディング」

では次に、モダンなJavaScriptの標準である ESM(`import` / `export`) を見てみましょう。
ファイルの拡張子を `.mjs` にするか、`package.json` に `”type”: “module”` を記述することで有効になります。

コードで比較してみる

// counter.mjs (ESM)
export let count = 0;

export function increment() {
count++;
}

// main.mjs (ESM)
import { count, increment } from ‘./counter.mjs’;

console.log(count); // 0 出力
increment();
console.log(count); // さあ、今度はいくつになるでしょうか?

ライブバインディングという魔法

このコードを実行すると、最後の `console.log` は `1` を出力します!

なぜCommonJSと挙動が違うのでしょうか?
ここにESMの真骨頂があります。ESMが提供するのは、値のコピーではありません。「ライブバインディング(Live Binding)」と呼ばれる、参照の紐付けです。

ESMの `import` は、読み込み元の変数への「直通回線(ポインタのようなもの)」を繋ぎます。そのため、モジュール内部で変数の値が書き換わると、それをインポートしている側からも、最新の書き換わった値がリアルタイムに見えるようになります。

[ counter.mjs ] [ main.mjs ]
let count = 1; <===========> import { count }
(値が変化すると) (即座に反映される!)

この違いは、アプリケーションの設計や状態管理(State管理)を行う上で非常に重要です。ESMを使うことで、モジュール間で「生きた状態」を安全に共有できるようになりました。

—

4. 陥りがちな文法エラーとデバッグの極意

初学者の開発現場で本当によくあるトラブルを2つご紹介します。ここを知っておくだけで、無駄なエラーに悩まされる時間を何時間も節約できますよ。

トラブル①:ESMで CommonJS のように動かそうとしてエラーになる

ESM環境で、つい慣れ親しんだ `require` や `module.exports` を書いてしまうミスです。

// エラーになる例 (ESM環境下)
const fs = require(‘fs’); // ReferenceError: require is not defined

【解決策】
Node.jsのESM環境では、必ず `import` を使いましょう。

import fs from ‘node:fs’;

(※ Node.jsの標準モジュールを読み込む際は、明示的に `node:` プレフィックスをつけるのが現代のベストプラクティスです)

トラブル②:ESMでインポートした変数を直接書き換えようとする

import { count } from ‘./counter.mjs’;

// やってはいけない!
count = 10; // TypeError: Assignment to constant variable.

ESMで `import` した変数は、読み取り専用(Read-only)のバインディングとして扱われます。
「ライブバインディング=外から書き換えられる」と勘違いしやすいのですが、あくまで「モジュール内部で起きた変更が外から見える」のであって、外側から勝手に元の変数を書き換えてよいわけではありません。変数を変更したい場合は、必ずエクスポートされた「関数(セッター)」経由で行う必要があります。

—

5. まとめ:今日から使えるアーキテクチャの視点

今回は、CommonJSとESMにおける変数の見え方の違いを、Node.jsの内部実装やメモリの仕組みを交えて解説しました。

  • CommonJS(`require`)
  • 変数は値の「コピー(スナップショット)」として渡される。
  • 柔軟だが、モジュール間の状態の同期には工夫が必要。
  • ESM(`import` / `export`)
  • 変数は「ライブバインディング(参照の紐付け)」として共有される。
  • モジュール内部の変化がリアルタイムに伝わり、モダンで堅牢な設計が可能。

JavaScriptのランタイムやV8エンジンが裏側でどうメモリを扱い、どうスコープを管理しているのかを意識できるようになると、コードを書く手が一段と楽しく、そして確実になります。

ここをクリアしたあなたなら、もうモジュールのスコープで迷うことはありません。自信を持って、次のモダンなコードを書き進めていきましょう!

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