こんにちは!JavaScriptの世界へようこそ。
フロントエンドのブラウザから、裏側を支えるNode.jsの深部まで、日夜コードを書き続けていると、「あれ? この変数はどこから見えているんだっけ?」と立ち止まる瞬間はありませんか?
特に、Node.jsで開発をしていると、ある時は `require` を使い、またある時は `import` を使う。その中で「ファイルごとに変数が独立しているのはなぜだろう」「`var` で宣言した変数が、なぜグローバルを汚染しないんだろう?」と疑問に思ったことはありませんか?
ここをクリアすれば、JavaScriptおよびNode.jsのモジュールシステムの本質はバッチリマスターできますよ。
今回は、V8エンジンが裏側で何をやっているのかという重厚な事実を、優しく、けれど本質的に紐解いていきましょう。
—
1. Node.jsのファイルは、そのままでは実行されない
私たちが普段何気なく書いているJavaScriptのファイル(`.js`)。
Node.jsは、このファイルを読み込んだとき、いきなりコードを上から順に実行しているわけではありません。
実はNode.jsは、あなたが書いたコードを実行する直前に、目に見えない「魔法のラッパー(包み紙)」でコード全体をぐるっと包み込んでいます。
まずは、CommonJS(`require` を使う世界)の裏側をのぞいてみましょう。
CommonJSのラッパー関数の正体
Node.jsがCommonJSのファイルをロードするとき、V8エンジンはあなたのコードを次のような形の関数でラップ(包囲)して評価します。
// 【概念モデル】Node.jsが裏側で自動的に生成しているラッパー関数
function (exports, require, module, __filename, __dirname) {
// あなたが書いたコードは、この関数の「中身」として配置されます!
// 例:あなたが written.js に書いたコード
const secretMessage = “秘密のデータ”;
module.exports = { secretMessage };
}
すごいですよね! 私たちがファイルの一番トップレベル(どの関数にも属さない場所)で `const` や `var` を使って変数を宣言したとしても、それは「ラップされた無名関数のローカル変数」でしかなかったのです。
これが、CommonJSにおいて「ファイルをまたいで変数が勝手に漏れ出さない(グローバルを汚染しない)」決定的な理由です。
実際に確認してみましょう
では、このラッパー関数が存在する証拠を、コードで確認してみましょう。
Node.jsでは、関数の中に入ると自動的に使える `arguments` や、先ほどの引数にどんな値が入っているかを確かめることができます。
// test.js
console.log(“— このファイルのパス —“);
console.log(__filename);
console.log(“— この変数はどこに属している? —“);
console.log(typeof module); // ちゃんとオブジェクトとして存在します
console.log(this === module.exports); // true になります
ファイルを実行すると分かりますが、`__filename` や `module` といった変数を、一度も宣言していないのにエラーなく使えますよね。それは、Node.jsがこのファイルを関数で包み、それらの値を用心深く「引数」として渡してくれているからです。
—
2. 変数の見え方が異なる理由:CommonJS vs ESM
さて、Node.jsにはもう一つの世界線があります。それが、モダンな標準規格である ESM(ECMAScript Modules / `import` と `export` を使う世界) です。
`require`(CommonJS)と `import`(ESM)では、変数の見え方や挙動の初期化プロセスが根本から異なります。それぞれの特徴を整理してみましょう。
CommonJSの変数の見え方
- 同期的な読み込み: コードを実行しながらその場でファイルを読み込みます。
- オブジェクトのコピー/参照の授受: `module.exports` に代入された値やオブジェクトがそのまま渡されます。
- スコープ: 先ほど見た通り、ファイル全体の関数スコープ(正確にはラッパー関数のローカルスコープ)に閉じ込められます。
ESMの変数の見え方
- 非同期的なパース・構築: ESMでは、V8エンジンはコードを実行する前に、まずファイル全体をくまなく解析し、どのような変数や関数がエクスポート/インポートされるかの「モジュールレコード(Module Record)」という設計図を事前に作成します。
- ライブバインディング(Live Binding): ここが一番のポイントです! ESMの `import` は、値のコピーをもらうのではなく、「元の変数のメモリへの直通回線(ライブ参照)」を繋ぎます。
言葉だけだと少し抽象的なので、コードと図解でその違いを体感してみましょう。
—
3. 実践!ESMの「ライブバインディング」を体験する
ESMで変数をインポートしたとき、元のファイルでその値が書き換わるとどうなるでしょうか。CommonJSでは値がコピーされるため影響しませんが、ESMでは「生きたリンク」がつながっているため、驚くべき現象が起きます。
プロジェクトの `package.json` に `”type”: “module”` を設定した環境、または `.mjs` 拡張子を使って試してみましょう。
カウンターの例(ESM)
// counter.js (エクスポート側)
export let count = 0;
export function increment() {
count++;
}
// main.js (インポート側)
import { count, increment } from ‘./counter.js’;
console.log(`初期値: ${count}`); // 初期値: 0
increment();
// なんと、インポートした側の count の値も自動的に更新されます!
console.log(`インクリメント後: ${count}`); // インクリメント後: 1
なぜこうなるのでしょうか?
ESMのモジュールレコード構築フェーズにおいて、V8エンジンはメモリ上に変数の置き場所を確保し、`import` 側と `export` 側でそのメモリアドレスを共有させます。そのため、片側で値が変われば、もう片側からもその変更がダイレクトに見える(ライブバインディング)というわけです。
初学者のうちによくある誤解として、「`import` した変数は `const` のように絶対に書き換えられない定数だ」と思いがちですが、実はインポート側からは再代入不可(Read-only)なだけで、エクスポート側で値が変われば、インポート側の値も連動して変化するという性質を持っています。ここを理解できていると、複雑なアプリケーションのデバッグで迷子にならずに済みますよ。
—
4. 陥りやすい文法エラーとデバッグの極意
変数のスコープやモジュールの仕組みを理解していないと、次のようなトラップにハマりがちです。現場で頭を抱えやすい代表的なエラーを見ておきましょう。
トラップ1: CommonJSで `this` の正体を勘違いする
先ほど、CommonJSのファイルはラッパー関数で包まれると言いました。では、そのラッパー関数内での `this` は何を指しているでしょうか?
// CommonJSのファイル内
console.log(this); // {} (空のオブジェクト、module.exportsを指す)
function checkThis() {
console.log(this);
//Strictモード(Node.jsのモジュール内はデフォルトでStrictモード)では undefined になります!
}
checkThis();
- 解決のヒント: 「ファイル直下の `this` は `module.exports` である」という事実を知らないと、オブジェクト指向的にコードを書こうとした際に意図しないバグを生みます。アロー関数やStrictモードの挙動と合わせて、スコープのコンテキストを常に意識しましょう。
トラップ2: ESMとCommonJSの混在によるエラー
Node.jsで `require` と `import` を安易に混ぜて使うと、以下のような有名なエラーに直面します。
> `ReferenceError: require is not defined in ES module scope`
- 理由: ESM(`”type”: “module”`)の世界では、V8エンジンはあの「CommonJSのラッパー関数」を用意してくれません。そのため、ラッパーの引数として渡されていた `require` や `module`、`__dirname` などの便利機能は存在しないのです。
- 解決策: ESM環境でディレクトリパス等を取得したい場合は、以下のように `import.meta.url` を使います。
import { fileURLToPath } from ‘url’;
import { dirname } from ‘path’;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname); // ESM環境での __dirname 代替
—
まとめ:モジュールシステムはV8とNode.jsの「優しさ」
いかがでしたでしょうか?
- CommonJS (`require`) は、ファイルを関数で包み込むことで、変数を安全にローカルに閉じ込め、同期的にデータをやり取りする仕組み。
- ESM (`import`) は、実行前にモジュールレコードを構築し、メモリへのライブバインディングによって効率的かつ安全に依存関係を結ぶモダンな仕組み。
一見すると難解なモジュールの挙動も、Node.jsの初期化プロセスとV8エンジンのメモリ管理の視点から覗いてみると、すべて理にかなった美しい設計であることが分かります。
ここをクリアできれば、もう変数のスコープやモジュール起因のエラーで迷うことはありません。自信を持って、次の機能開発へ進んでくださいね! 応援しています!