module.exports は、Node.jsのファイルが require() で自分を読み込んだほかのファイルに渡す値です。最初は空のオブジェクトで、複数のものをエクスポートするならプロパティを追加し、1つだけ(関数やクラス)をエクスポートするなら新しい値を代入します。exports は同じオブジェクトを指す省略用の変数なので exports.add = ... は動きますが、exports = ... はつながりを切ってしまい、何もエクスポートされません。
5 20 3.14159、続いて [ 'add', 'multiply', 'PI' ] が表示されました。math.js の3行は、module.exports 経由でも exports 経由でも同じオブジェクトにプロパティを足していて、require('./math') はそのオブジェクトを返しました。
関数、クラス、オブジェクトをエクスポートする
モジュールの中心が1つだけなら、それを module.exports に直接代入します。呼び出す側はオブジェクトに包まれたものではなく、関数そのものを受け取ります。
Hello, Ada と Grace (user) が表示されました。TypeError: greet is not a function が出たら、呼び出す側の受け取り方が間違っています。オブジェクトのプロパティなら const { greet } = require(...)、代入された値なら const greet = require(...) と書きます。
module.exportsとexports:再代入の落とし穴
ファイルが実行される前に、Node.jsは exports = module.exports を設定します。どちらの名前も1つのオブジェクトを指し、require が返すのは module.exports です。exports に新しい値を代入しても、変わるのはローカル変数だけです。
broken.js は {} をエクスポートしました。exports が、誰も返さない新しいオブジェクトを指すようになったからです。working.js は { version: 2 } をエクスポートしました。
| 書き方 | エクスポートされる? |
|---|---|
module.exports.x = 1 | される |
exports.x = 1 | される(同じオブジェクト) |
module.exports = { x: 1 } | される(オブジェクトを置き換える) |
exports = { x: 1 } | されない(値は失われる) |
いちばん簡単なルールは、いつも module.exports と書くことです。どの場合でも動き、コードが何をしているかもそのまま読めます。
requireはキャッシュされた同じオブジェクトを返す
Node.jsはモジュールのコードを最初にrequireされたときに1回だけ実行し、module.exports をキャッシュします。同じファイルを後からrequireすると毎回そのオブジェクトが返るので、モジュールの中の状態は、それを読み込むすべてのファイルで共有されます。
counter.js is running は1回だけ表示され、b.next() は 3 を返し、a === b は true です。キャッシュのキーは解決後のファイルパスで、./counter と ./counter.js は同じファイルに解決されるからです。あるモジュールで作ったデータベース接続や設定オブジェクトがアプリ全体で共有されるのはこのためです。
module.exportsとESモジュール
ESモジュールでは代わりに export と import を使います。書き方は次のように対応します。
// CommonJS // ES modules
module.exports = greet; export default greet;
module.exports.add = add; export { add };
const greet = require('./greet'); import greet from './greet.js';
const { add } = require('./math'); import { add } from './math.js';
ESモジュールはCommonJSのファイルを import できます。その module.exports はデフォルトインポートとして届きます。Node.js 22.12以降は、トップレベルの await を持たないESモジュールなら require() でも読み込めます。どちらを選ぶかはCommonJSとESモジュールを参照してください。
よくある間違い
1つのファイルで exports.x と module.exports = ... を混ぜる。 代入でオブジェクトが置き換わるので、それより前に書いた exports.x はすべて消えます。書き方はファイルごとに1つに決めましょう。
後で変わる値をエクスポートする。 module.exports = { count } は読み込み時点の数値をコピーします。モジュールが count を更新しても、呼び出す側には古い値が見えたままです。上の counter.js のように、値を返す関数をエクスポートしてください。
./ を書き忘れる。 require('math') は、隣にある math.js ではなく node_modules のパッケージを探します。ローカルのファイルは必ず ./ か ../ で始めます。
よくある質問
1つのファイルでmodule.exportsとexportを両方使えますか?
使えません。ファイルはCommonJS(require、module.exports)かESモジュール(import、export)のどちらかです。Node.jsは拡張子(.cjs か .mjs)か、package.json の "type" で判断します。
requireが空のオブジェクトを返すのはなぜですか?
たいていは、モジュールが module.exports = ... ではなく exports = ... と代入しているか、2つのモジュールがお互いをrequireしています。循環requireでは、片方のファイルが実行を終える前に、もう片方がその module.exports を受け取ってしまいます。
requireに.jsの拡張子は必要ですか?
不要です。require('./math') は ./math、次に ./math.js、./math.json、./math.node、最後に index.js を持つフォルダ ./math の順に探します。ESモジュールの import では拡張子が必須です。
requireがキャッシュしたモジュールを読み込み直すには?
delete require.cache[require.resolve('./math')] でエントリを消してから、もう一度requireします。役に立つのは主にテストや開発ツールで、アプリのコードはこれに頼るべきではありません。