ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

Node.js : exports と module.exports の違い(解説編)

JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース) : ATNDも皆さんのご協力で25日間終わり、無事新しい年が迎えられそうです。参加された方、ご苦労様でした。もしアドカレに穴が空きそうだったら書いてみようと思ってたネタを作っていましたので、アドカレ終了記念の番外編で書いてみます。

ちょっと前のブログになりますが、Node.js Module – exports vs module.exportsな記事が掲載されていました。 Node.js のモジュールを作成する際に使用する exports 変数と module.exports 変数の違いについての記事です。私も以前から「 module や exports って変数はいったい何だろう?」とか、「require()関数って突然どこから現れてくるのだろうか?」など実際その仕組みはどうなのか気になっていました。

Node.jsのマニュアルの該当箇所http://nodejs.jp/nodejs.org_ja/docs/v0.6/api/globals.html#moduleでは、

module
現在のモジュールへの参照です。特に module.exports は exports オブジェクトと同じです。より詳しくは src/node.js を参照してください。 module は実際はグローバルではなく、各モジュール毎のローカルです。
exports
現在のモジュールの全てのインスタンス間で共有されるオブジェクトで、 require を通じてアクセス可能になります。 exports は module.exports と同じオブジェクトです。より詳しくは src/node.js を参照してください。 exports は実際はグローバルではなく、各モジュール毎のローカルです。

と書かれており、わかったようなわからないような src/node.js を見るのかぁ〜と避けながら、他の人の書いたモジュールを見てなんとなく使い方を見て習えのような感じでした。

話を先のブログ記事に戻すと、タイトルの通り Node.jsの exports と module.exports の違いについて書かれています。
この記事中では exports と module.exports の違いを

module.exports is the real deal. exports is just module.exports's little helper.
module.export が real deal(本体) です。 exports は module.exportsの little helper(ちょっとした助手)です。

と表しています。
例として(ちょっと書き直しています)JavaScriptのモジュールファイル(mymodule.js)を下記のように書き、 exports 変数にメソッド(name)を定義すると、
./mymodule.js

exports.name = function() {
  console.log("My Name is jovi0608.");
};

無事 require して取得したオブジェクトで name メソッドが使用できます。

unix:~> node -e "var mymod = require('./mymodule.js'); mymod.name();"
My name is jovi0608.

一方、さっきのモジュールファイル中で module.exports に文字列など代入してみると急に name メソッドが使えなくなってしまいます。
./mymodule.js

module.exports = "Hi";
exports.name = function() {
  console.log("My Name is jovi0608.");
};
unix:~> node -e "var mymod = require('./mymodule.js'); mymod.name();"

undefined:1
^
^
TypeError: Object Hi has no method 'name'
    at Object.<anonymous> (eval at <anonymous> (eval:1:82))
    at Object.<anonymous> (eval:1:70)
    at Module._compile (module.js:432:26)
    at startup (node.js:80:27)
    at node.js:545:3

この際 require() で取得したオブジェクトは module.exports に代入した "Hi" がはいっています。そして module.exports にコンストラクター関数や配列を代入した例を示して、記事のまとめとして

So you get the point now - if you want your module to be of a specific object type, use module.exports; if you want your module to be a typical module instance, use exports.
もうお分かりですね。もしモジュールを特定のオブジェクト型にしたいなら module.exports を使いなさい。通常の module インスタンスとして使うなら exports を使いなさい。

という風に exports と module.exports の変数の使い分けを説明しています。

require() で得られる値を普通のオブジェクトにしたいなら exports のプロパティを追加していくようにすればいいし、 require()の戻り値をコンストラクター関数や配列・文字列など別のものにしたいなら module.exports にしろと。まぁこれで大分使い方が理解できましたが、ちょっと納得いきませんね。なので今回 exports と module.exports の違いについて*より詳細*な解説をしてみます。(相変わらず前フリが長かったです。)

説明を始める前に exports と module.exports の違いがわかるいくつかの挙動を見てみます。(下記で記載されている JavaScriptのモジュールはいずれも mymodule.js というファイル名で保存されています。)

[Case1] exports, module.exports のプロパティに変数を代入

require() には設定された両方の値が反映される。

exports.hoge = "hoge";
moduel.exports.foo = "foo";
unix:~> node -e "console.log(require('./mymodule.js'));"
{ hoge: 'hoge', foo: 'foo' }
[Case2] module.exports にオブジェクト、exports のプロパティに変数を代入

require() には module.exports の設定値のみ反映される。

exports.hoge =  "hoge";
module.exports = {foo: "foo"};
unix:~> node -e "console.log(require('./mymodule.js'));"
{ foo: 'foo' }
[Case3] exports にオブジェクト、module.exports のプロパティに変数を代入

require() には module.exports の設定値のみ反映される。

exports = {hoge : "hoge"};
module.exports.foo = "foo";
> node -e "console.log(require('./mymodule.js'));"
{ foo: 'foo' }
[Case4] exports, module.exports の両方にオブジェクトを代入

require() には module.exports の設定値のみ反映される。

exports = {hoge :  "hoge"};
module.exports = {foo: "foo"};
unix:~> node -e "console.log(require('./mymodule.js'));"
{ foo: 'foo' }

ホント module.exports 強いです。
「圧倒的ではないか我が軍は!」

この挙動の違いをマニュアルに書いてある通り src/node.js から追っていきます。といっても初めから全部説明するのは膨大すぎるのでポイントとなるところは、

src/node.js(v0.6.6)
   526    NativeModule.wrap = function(script) {
   527      return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
   528    };
   529
   530    NativeModule.wrapper = [
   531      '(function (exports, require, module, __filename, __dirname) { ',
   532      '\n});'
   533    ];
   534
   535    NativeModule.prototype.compile = function() {
   536      var source = NativeModule.getSource(this.id);
   537      source = NativeModule.wrap(source);
   538
   539      var fn = runInThisContext(source, this.filename, true);
   540      fn(this.exports, NativeModule.require, this, this.filename);
   541
   542      this.loaded = true;
   543    };

のところです。
ファイルから読み込んだモジュールをコンパイル・実行(runInthisContext: これは vm.runInthisContextと同じです。)する際にモジュールの JavaScriptソースを wrap しているのがわかります。

(function(exports, require, module, __filename, __dirname) {
  モジュールファイル内の JavaScript コード
});

そうです。 モジュール内で使っている exports, require, module などの変数は、モジュールを実行するラッパー関数の引数として渡された変数だったんです。

本当は lib/module.js 内で処理されているのですが、module を require() している疑似コードは下記のように書けます。(filename,dirnnameは省いています。)

function Module() {
  this.exports = {};
  ...
}
Module.require = function(id) {
  var module =  new Module(id);
  (function (exports, require, module) {
     モジュール内コード
     exports = ... 
     module.exports = ...
  })(module.exports, Module.require, module);
  return module.exports;
}
// こっから本体     
var require = Module.require;
var mymod = require("./mymodule.js");

上記の疑似コードをみると次のことがわかります。

  • require() するとその関数スコープ内で module オブジェクトが毎回インスタンス化される。
  • その際 module.exports = {} として空オブジェクトに初期化される。
  • モジュール内のコードは無名関数にラップされ module.exports オブジェクトは exports という名前の関数引数としてモジュール内コードに渡される。
  • require() 関数からの戻り値は、 module.exports であり exports 変数ではない。

関数へオブジェクトを渡した場合の挙動は、https://developer.mozilla.org/en/JavaScript/Reference/Functions_and_function_scope#General にあるよう、

However, object references are values, too, and they are special: if the function changes the referred object's properties, that change is visible outside the function, as shown in the following example:
しかしながら、オブジェクトへの参照は特別な値渡しである。もし関数が参照オブジェクトのプロパティを変更したら、その変更は次の例にあるよう関数外でも有効になります。

となっています。(ここも詳しく説明すると深いです。call by sharing とも言われています。詳細は http://dmitrysoshnikov.com/ecmascript/chapter-8-evaluation-strategy/#ecmascript-implementation を参照してみるといいです。)
よって exports には module.exports = {} の空のオブジェクトが渡されているので、そのオブジェクトのプロパティを変更した時のみ module.exports に反映されて require() の戻り値として扱うことができるということです。(関数へのオブジェクト渡し Call by sharing のため)

これでちゃんと納得できました。

ということで仕組みが分かった以上圧倒的に負けていた exports にも逆転の目が出てきました。

[Case5] moduleにオブジェクト、exports のプロパティに変数を代入

require() には exports の設定値のみ反映される。

exports.hoge = "hoge";
module = {exports: {foo: "foo"}};
unix:~> node -e "console.log(require('./mymodule.js'));"
{ hoge: 'hoge' }

おぉ! この状況では exports は module.exports に勝っています。
でも、

[Case6] module と exports にオブジェクトを代入
exports = {hoge : "hoge"};
module = {exports: {foo: "foo"}};
unix:~> node -e "console.log(require('./mymodule.js'));"
{}

両軍とも全滅しちゃいましたww