Node.js : exports と module.exports の違い

weixin_33709609發表於2012-07-25

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. (eval at  (eval:1:82))
    at Object. (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'));"
{ hoge: 'hoge', 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

相關文章