Edited at

# CommonJS と ES6の import/export で迷うなら

More than 3 years have passed since last update.


はじめに

いつもはes6のモジュール管理方式であるimport/exportを使って、npmで手に入れたライブラリや自分で作成したモジュールをロードしているが、たまに思った挙動にならないことがある。また、export defaultしたモジュールをテストしようとしてkarmaでrequire()するとエラーになったりした。そういう場合は、とりあえずmodule.exportsを使ってみるとうまくいったりすることが多いのだが、なぜなのかはあまり考えていなかった。

ということで、実際にはどうなっているのか、またモジュールシステムとは何なのかという点で基本的なことから理解を深めて、es6のimport/exportとの違いを知り、より正しく実装できるようになれば良いと思っている。


結論としては

方針としては基本的にはES6形式で記述するが、CommonJS形式の読み書きが必要なケースは、次の対比表を使って理解する。


対比表

CommonJS
Export/Import
備考

exports.method = method
export { method }

module.exports = function() {...}
export default function() {...}

export.default = function() {...}
export default function() {...}
CommonJS的には、いちプロパティの default に対して関数を設定するのと同じこと

var a = require('module')
import a from 'module'

var a = require('module').a
import { a } from 'module'

var a = require('module').default
import a from 'module'

export defaultでエクスポートされたモジュールの場合


もう少し詳しく


Modules

Node.js にはシンプルなモジュールローディングシステムがある。ファイルとモジュールとは、一対一の関係がある。例えば次のようなsampleA.jsは、sampleAモジュールとみなすことができる。


sampleA.js

const PI = Math.PI;

exports.area = (r) => PI * r * r;
exports.circumferenct = (r) => 2 * PI * r;

exportsという特別なオブジェクトのプロパティとして、エクスポートしたい関数を追加すれば良い。


module.exports

もし、関数そのもの(例えばコンストラクタ)をエクスポートしたかったり、オブジェクトをエクスポートしたい場合は、module.exportsキーワードを用いる。なお、モジュールシステム自体は、require('module')にて実装されている。


The module object

module変数は、現在のモジュールを表現しているオブジェクトを参照している。便宜上、module.exportsexports経由でもアクセス可能と なっているが、続く項にあるようにexportsオブジェクト自体の参照を更新すると問題が生じる。


module.exports

module.exportsオブジェクトはモジュールシステムによって作られる。我慢できないことはないが、ライブラリ作成時にあるクラスのインスタンスをモジュールとしてエクスポートしたいニーズは多くある(newする、の意)。そうするためにはmodule.exportsに望みのオブジェクトを代入する。この時exports変数にオブジェクトを代入しても、エクスポートしたことにならないので注意する


exports alias

exports変数はモジュールの内部で利用可能であり、module.exportsの参照としてそのライフサイクルが始まる。もし新しい値を代入すれば前の値はバインドされることはないし、何もエクスポートされなくなる。ガイドとしては、もしこの関係性が奇妙に映るようならexportsは無視してmodule.exportsを使え、ということだ。


Export(ES6)

エキスポート文は、関数やオブジェクトやプリミティブ型の値をモジュールやファイルからエクスポートするときに使う。


Named exports

Named exports は、幾つかの値をエクスポートするときに便利で、インポートする側は、参照する際に同じ名前を使うことができる。


my-module.js

// module "my-module.js"

export function cube(x) {
return x * x * x;
}
const foo = Math.PI + Math.SQRT2;
export { foo };

別のスクリプトから、次のように読み込むことができる。


index.js

import { cube, foo } from 'my-module';

console.log(cube(3)); // 27
console.log(foo); // 4.555806215962888


Default export(only one per script)

default export においては、1モジュールにつき一つの初期エクスポートが存在している。これには、関数やクラス、オブジェクトなど何でも設定できる。


my-module.js

// module "my-module.js"

export default function cube(x) {
return x * x * x;
}


index.js

import cube from 'my-module';

console.log(cube(3)); // 27


export default / require()

この組み合わせでエクスポート・インポートする場合は、次のように考える。


index.js

var a = require('./module').default


index.jsにて、importするならこう。


index.js

import a from './module'


aに対して、defaultを明示しているかどうかの違いがある。

この部分の取り扱いの違いが、karmaでテストする際のエラー(冒頭に記述した問題点)の原因であり、export defaultと、module.exports =は等価なものだという思い込みが諸悪の根源だった。


参考URL

Node.js v6.2.2 Documentation

JavaScriptとECMAScript、CommonJS/AMD/Browserify/RequireJS/Webpack、TypeScriptに関する覚え書き