概要
恐れながら言わせてもらうけれど、 CommonJS のエクスポートの理解に苦労したのはひとえにネットの記事の書き方が悪いせいだな。
いやその、ネットの記事にはいつも大変お世話になっているよ? ただし CommonJS のエクスポートの説明についてはヒドいな。ぼくの説明が世界でいちばん分かりやすいと思うね!
“CommonJS の2通りのエクスポート” っつーのは
// まずは module.exports。
// よくある説明: これだとインポート (require) する側が
// 自由に名前を変更できてしまう。
module.exports = (i) => i + 1;
// よくある説明: 自由な名前変更を避けたいときはこの exports を使う。
exports.increment = (i) => i + 1;
この、 "module.exports
ではなく exports
を使う" みたいな言い方よ。 exports
は module.exports
のショートカットなので同じものなのに、まるで別のものであるかのような誤解を招くのだよ。
緑さんの理解では CommonJS のエクスポートはこうだ
- CommonJS の実行環境では、ファイルひとつにつき “モジュールスコープ変数” たちが自動でつくられる。 Python でいうところのビルトイン変数のことだ。
- その一覧はここ (https://nodejs.org/api/modules.html#the-module-scope) にあるぜ。
- この件に関わるモジュールスコープ変数は以下の3つ。
module
: これはオブジェクトで、中にexports
というプロパティ (中身は空っぽのオブジェクト) を持つ。exports
: これはmodule.exports
↑への参照。require
: これはまあ基本的には関数。別ファイルのmodule.exports
を参照する。
“CommonJS の実行環境では、 require()
を使って別のファイルの module.exports
へアクセスできる”。 ぼくの理解では、 CommonJS のエクスポート、インポートの仕組みはこれがすべてだ。2種類の方法なんて、ない。
以下、実験ノート
以上の理解に至った実験ノートを残しておこう。
node -v
# v20.3.0
# これは Python でいうところの python -c みたいなもの。
node --eval 'console.info({ module })'
# {
# module: Module {
# id: '[eval]',
# path: '.',
# exports: {},
# filename: '/path/to/here/[eval]',
# loaded: false,
# children: [],
# paths: [
# ...
# '/node_modules'
# ]
# }
# }
このとおり、 Node.js (CommonJS) の実行環境では、モジュールスコープ変数の module
が自動で定義されている。まあ Python でいうところの __name__
みたいなものだよ。
さて、事をややこしくしているのは exports
というモジュールスコープ変数の存在だよな。ただ、コイツは module.exports
への参照でしかない。それを見てみようぜ。
node --eval 'module.exports.foo = 1; exports.bar = 2; console.info({ exports })'
# { exports: { foo: 1, bar: 2 } }
ほらね。個人的には、ぜーんぶ module.exports
を使うことにして、 exports
は封印したほうがいいと思うよ。
そして、 require()
は別ファイルの module.exports
の内容をそのまま返してくれる。
echo 'exports.increment = (i) => i + 1; module.exports.sunday = "Sunday";' > foo.js
node --eval 'console.info(require("./foo.js"))'
# { increment: [Function (anonymous)], sunday: 'Sunday' }
そういうわけで、 CommonJS には2種類のエクスポート方法があるというのは世迷い言だ。デフォで用意されている require
関数が、別ファイルの中の、これまたデフォで用意されている module
オブジェクトの module.exports
プロパティにアクセスすることができる。これがすべて。