読者です 読者をやめる 読者になる 読者になる

Node.jsのexportsについて

Node.js

Node.jsにはモジュール機能があり、他のファイルの内容を読み込むことが出来る。
だが、何となくでしか使い方を理解していなかったので、調べてみた。

このエントリのコードは全て、v4.4.3で動かした。

基本

まず、呼び出される側のファイルで、モジュール化する値やオブジェクトをexportsしておく。
そうすることで、その値やオブジェクトに外部からアクセスすることが可能になる。

呼び出す側ではrequire()関数を使い、ファイルを読み込む。
読み込んだファイルから、exportsされている値やオブジェクトにアクセスできるようになる。

// parts.js
var value = '値';
var foo = 'foo';
exports.value = value;

// index.js
var parts = require('./parts.js');
console.log(parts.value);   // 値
console.log(parts.foo); // undefined

ファイルを読み込んでも、exportsされていない値(上記のケースではfoo)には、アクセス出来ない。

下記のような書き方も出来る。

// parts.js
module.exports = {
    value: '値',
    foo: 'foo'
};

// index.js
var parts = require('./parts.js');
console.log(parts.value);   // 値
console.log(parts.foo); // foo
// parts.js
module.exports.value = '値';
module.exports.foo = 'foo';

// index.js
var parts = require('./parts.js');
console.log(parts.value);   // 値
console.log(parts.foo); // foo

module.exportsとexports

module.exportsexportsという書き方が出てきた。
何が違うのだろうか。
ネットで軽く調べた限り、同じものとして扱って構わないらしい。

exports = module.exports = {}

という図式であり、exportsmodule.exportsを参照している、らしい。

だから、

exports.hoge = 'hoge';
exports.fuga = 'fuga';
.
.
.

という書き方をしていれば、exports = module.exportsの図式は壊れない。

だが例えば、module.exportsに新しくオブジェクトを割り当ててしまうと、exports = module.exportsのリンクは途切れてしまう。

それにより、以下の様な挙動が発生する。

// parts.js
var value = '値';
var foo = 'foo';
module.exports = {
    fuga: 'fuga'
};
exports.value = value;

// index.js
var parts = require('./parts.js');
console.log(parts.value);   // undefined
console.log(parts.fuga);    // fuga

なぜvalueundefinedになってしまうのかと言えば、module.exportsに新しくオブジェクトを割り当ててしまい、リンクが途切れ、module.exportsexportsはそれぞれ別のものに指すようになってしまったから。

parst.jsを実行させてみることで、そのことが確認出来る。

var value = '値';
var foo = 'foo';
console.log(module.exports === exports);    // true
module.exports = {
    fuga: 'fuga'
};
exports.value = value;
console.log(module.exports === exports);    // false

元々両者は同じものを指していたが、module.exportsの再定義によってそれが変わってしまっている。

そして、require()で読み込むのは、読み込んだファイルのmodule.exportsである。
そのため、この状態では、exportsにいくら値を設定しても、外部ファイルがそれを呼び出すことは出来ない。

まとめると、

  • module.exportsというオブジェクトを、require()によって外部ファイルが呼び出すことが出来る
  • 初期状態では、exportsmodule.exportsを参照しており、両者は同じものを指している

ということになる。

require()すると、対象となるファイルの中身が実行される

require()は、ただ単にmodule.exportsを取得するだけでなく、そのファイルの中身を実行する。

// parts.js
console.log('ファイルが読み込まれました。');

// index.js
var parts = require('./parts.js');  // ファイルが読み込まれました。
console.log(parts); // {}
// 以下は何も表示されない
require('./parts5.js');

ファイルの中身を実行してmodule.exportsを返す、というのがrequire()の挙動のようだ。

注意点として、require()は、同じファイルに対しては1度しか実行されないようだ。
そのため上記のケースでは、2回目のrequire()では何も表示されなかった。

// parts.js
var int = 0;
int++;
module.exports.int = int;

// index.js
var parts = require('./parts.js');
console.log(parts.int);   // 1
require('./parts5.js');
console.log(parts.int); // 1

require()の結果を別の変数に割り当てようとしても同様。require()自体が1度しか実行されない。

// parts.js
var int = 0;
int++;
module.exports.int = int;

// index.js
var parts = require('./parts.js');
console.log(parts.int);   // 1
var parts2 = require('./parts5.js');
console.log(parts2.int); // 1

この、「require()は同一ファイルに対して1度のみ実行され」、「その結果は記憶されている」という仕様により、次のような特徴が生まれる。

クロージャ

記述の通り、module.exportsというオブジェクトに入っていない値は、呼び出せない。
しかし、module.exportsを通して間接的にアクセスすることは出来る。

// parts.js
var int = 10;
function myFunc(arg){
    console.log(arg * int);
};
module.exports.myFunc = myFunc;

// index.js
var parts = require('./parts.js');
console.log(parts.int);   // undefined
parts.myFunc(2);    // 20

上記の例では、エクスポートされているのはmyFuncのみ。だから、エクスポートされていないintにアクセスしようとしてもundefinedになる。
しかしmyFuncにおいてintを使っているため、myFuncを通じて間接的にアクセスできる。

エクスポートしていなくても、必要に応じて値を読みに行く。これは、関数やオブジェクトでも同様である。

// parts.js
function add(a,b){
    return a + b;
};
function showResult(a,b){
    console.log( add(a,b) );
};
module.exports.showResult = showResult;

// index.js
var parts = require('./parts.js');
parts.showResult(2,3);  // 5
console.log('showResult' in parts);  // true
console.log('add' in parts); // false

showResult()の中で、エクスポートされていないadd()が使われているが、問題なく動く。
だが、add()そのものがpartsに入っているわけではない。あくまでも、必要になった時(この場合はshowResult()実行時)に呼び出されるだけである。

この特徴を利用すると、クロージャのような仕組みを作ることが出来る。

// parts.js
var int = 0;
function increment(){
    int++;
};
function get(){
    return int;
};
module.exports.increment = increment;
module.exports.get = get;

// index.js
var parts = require('./parts.js');
console.log(parts.inc); // undefined
console.log(parts.get());   // 0
parts.increment();
console.log(parts.get());   // 1

変数intはエクスポートされていないため、それに直接アクセスすることは出来ない。
だが、関数get()increment()を通して、操作、取得することが出来る。

ディレクト

require()の引数にディレクトリを渡すことも出来る。
そうすると、そのディレクトリのindex.jsを読み込む。
その時、index.jsで他のファイルを読み込みエクスポートしておくことで、複数のファイルを簡単に読み込める。

// ディレクトリ構造
- app.js
- modules/
    - index.js
    - data.js
    - cal.js
// data.js
var int = 1;
module.exports.int = int;

// cal.js
function add(a,b){
    return a + b;
};
module.exports.add = add;

// index.js
module.exports.cal = require('./cal.js');
module.exports.data = require('./data.js');

このような状況では、以下の様な書き方をすることで、data.jscal.jsにアクセスすることが出来る。

// app.js
var myModules = require('./modules');
console.log(myModules.data.int);  // 1
console.log( myModules.cal.add(myModules.data.int,2) );   // 3

参考記事