JavaScriptのモジュールは変数をエクスポートする

今時のJavaScript開発において、JavaScriptが持つ**モジュール**の機能は欠かすことができません。我々はプログラムをいくつものファイル（モジュール）に分割し、`import`文と`export`文を使ってそれらを繋げています。各モジュールは`export`文を用いてそのモジュール内で定義した変数・関数などをエクスポートすることができ、別のモジュールが`import`文でそれらの値を取得することができるのです。

皆さんは、この`import`・`export`文がどのように働いているのか正確に説明できるでしょうか。実は、`import`文や`export`文というのは値をインポート・エクスポートしているのではなく、言わば**変数そのもの**をインポート・エクスポートしているのです。これを理解するのがこの記事のゴールです。

※ 本当は変数ではなく「バインディング」といったほうが用語としてより正確なのですが、この記事では分かりやすさのために変数という用語を使用しています。

## `export`文は変数をエクスポートする

モジュールから何かをエクスポートするときに使うのが`export`文ですが、これにはいくつかの種類があります。実は、`export`文は必ず**変数**をエクスポートしています。

最も代表的な`export const foo = ...;`という構文は、`const`宣言により変数`foo`を作ると同時にそれをエクスポートしています。`export let bar = ...`のように`const`以外を使うことも可能です。それどころか、`export const {foo, bar} = obj;`のように分割代入で作られる変数をエクスポートすることもできます（この場合は`foo`と`bar`がエクスポートされます）。

ほかに`export function 関数名() { }`という関数宣言の形もよく使われます。この場合も、関数宣言を通してその関数が入った変数が作られています。

### 変数に外向きの名前をつけてエクスポートする

また、`export { foo, bar }`という構文は、事前に定義された変数`foo`、`bar`をエクスポートするという意味ですから、やはり変数をエクスポートしています。この構文の特徴は`export { foo as foo2 };`のように違う名前でエクスポートする機能を持っている点です。これにより「モジュールの中では`foo`と呼ばれている変数を、外向きには`foo2`という名前でエクスポートする」ということが可能です。このように、モジュールからエクスポートされている変数は「内向きの名前」と「外向きの名前」を持ちます。`export const foo = ...`のような宣言の場合は内向きの名前と外向きの名前が同じで、どちらも`foo`です。

### defaultエクスポートの扱い

ところで、`export default 値;`という構文では変数をエクスポートしていないように見えますね。しかし、実はこれは内向きには`*default*`という名前の変数を作成し、外向きに`default`という名前でエクスポートしています。ここで作られた`*default*`という名の暗黙の変数は、そんな変な名前の変数にアクセスする構文的な手段が存在しないため、モジュール内からアクセスすることはできません。

defaultエクスポートは`default`という名前でエクスポートする機能です。逆に言えば、`export default`以外の方法でも`default`という名前でエクスポートすればdefaultエクスポートと同様の挙動をするということです。次の`a.mjs`と`index.mjs`を用意して`index.mjs`を実行すれば、コンソールに`something is 123`と表示されるでしょう。

```js
////////// a.mjs
const bar = 123;
export {
  bar as default
};


////////// index.mjs
import something from "./a.mjs";

console.log("something is", something);
```

このように、`export default`といったdefaultエクスポートの構文は実は「`default`という名前をつけてエクスポートする」という行為を短く書くだけの構文なのです。

ちなみに、「変数を`default`という名前でエクスポートする」というのはこの例のように`as default`を使わないとできません。`default`は予約語であり、`export const default = ...`のような気がしてやり方だと構文エラーとなってしまうのです。うまくできていますね。

### 再エクスポートの構文

`export`文は再エクスポートの機能も持っています。まず、`export * from "module"`はモジュールからエクスポートされた変数を同じ名前でエクスポートします。面白いのは、この構文は自身のスコープ内にその変数を作らないということです。下の例では`b.mjs`は`foo`という名前で変数をエクスポートしています。`a.mjs`内の`export * from "./b.mjs";`は`b.mjs`からエクスポートされている`foo`を同じ`foo`という名前で再エクスポートするという働きをします。言い換えれば、`a.mjs`は「`b.mjs`の`foo`」を`foo`という外向きの名前でエクスポートしているのです。

再エクスポートが普通のエクスポートと決定的に違う点は、「自身のスコープの変数」をエクスポートするのではなく「他のモジュールの変数」をエクスポートしているという点です。これはつまり、再エクスポートは自身のスコープに何の影響も与えないということです。`a.mjs`の中で`foo`という変数が宣言されていたとしても、再エクスポートされている`foo`とはまったく無関係です。

```js
////////// b.mjs
export const foo = 123;

////////// a.mjs
// b.mjsのfooを再エクスポート
export * from "./b.mjs";

// この変数fooはb.mjsがエクスポートするfooとは無関係
const foo = 0;
console.log(foo);

////////// index.mjs
import { foo } from "./a.mjs";

// このfooはb.mjsのfooなので123が表示される
console.log(foo);
```

この例を見ると、`a.mjs`内の`console.log(foo)`は0を表示します。これは、`a.mjs`のスコープにある`foo`はそのモジュール内で宣言されている変数`foo`だからです。一方、`index.mjs`内の`console.log(foo)`は123を表示します。これは、この`foo`が`a.mjs`からインポートした`foo`であり、`a.mjs`が`foo`という名前でエクスポートしているのは「`b.mjs`がエクスポートする`foo`」だからです。

ちなみに、`a.mjs`の中で`export const foo = 0`のように書いた場合は`export *`の方よりも優先されてこちらが`foo`としてエクスポートされます。

他に`export * as ns from "module"`構文や`export { foo, bar } from "module"`構文が再エクスポートを行いますが、これらも現在のモジュールのスコープ内には影響を与えません。

また、`default`という名前でエクスポートされている変数は`export * from "module"`構文で再エクスポートされません。defaultエクスポートを再エクスポートしたければ`export { default } from "module"`という方法が有効です。

## `import`文は変数をインポートする

`export`文が変数をエクスポートするなら、`import`文がインポートするのも当然変数です。そのことがたいへんよく分かる例がこれです。

```js
////////// a.mjs
export let foo = 0;

export const setFoo = (value) => {
  foo = value;
}

////////// index.mjs
import { foo, setFoo } from "./a.mjs";

// a.mjs内のfooは0なので0が表示される
console.log(foo);

// a.mjs内のfooが100になる
setFoo(100);

// a.mjs内のfooは100なので100が表示される
console.log(foo);
```

この例では`a.mjs`が変数`foo`をエクスポートし、`index.mjs`がインポートしています。すると、`index.mjs`のスコープに存在する`foo`は`a.mjs`に存在する`foo`と同じになります。より正確には、`index.mjs`に存在する変数`foo`は、「参照されると`a.mjs`の変数`foo`の中身を返す変数」となります。これが意味することは、`index.mjs`の変数`foo`の値は常に`a.mjs`の変数`foo`の値と同じであるということです。

このことは、`a.mjs`が提供する`setFoo`関数を用いて`a.mjs`内の変数`foo`を書き換えると分かります。`setFoo`の呼び出し後は、`index.mjs`の変数`foo`の中身が勝手に変わっています。これはもちろん、`a.mjs`の変数`foo`の中身が変わったからです。

ここで重要なのは、**インポートは値のコピーではない**ということです。あくまで変数そのものをインポートしているのであり、だからこそ、インポート後に元の変数の値が変わっても追随できるのです。言い方を変えれば、**インポートはモジュール間で変数のエイリアスを作る機能**であるとも言えます。上の例では、`index.mjs`内の変数`foo`は`a.mjs`内の変数`foo`のエイリアスであるとの見方もできますね。JavaScriptのモジュール間連携とは、モジュールの間に張られたエイリアスによって成り立つものなのです。

ただし、変数に再代入できるのはその変数を所有するオリジナルのモジュールだけです。`index.mjs`で`foo = 123;`のようにして`a.mjs`内の変数`foo`を書き換えることはできません（ランタイムエラーになります）。インポートされた変数は読み取り専用のエイリアスなのです[^note_alias]。

[^note_alias]: エイリアスという言葉を用いた説明は分かりやすいので使っていますが、公式の用語ではないので注意してください。仕様書上の定義は、最初の述べた「参照されるとインポート元の変数の値を返す変数」というものです。

一応、インポートと対比して、明示的に値をコピーする例も用意しておきます（`a.mjs`の中身は同じなので省略）。

```js
////////// index.mjs
import { foo, setFoo } from "./a.mjs";

// これを実行した時点でのfooの値をmyFooに代入
const myFoo = foo;
// 0が表示される
console.log(myFoo);

// a.mjs内のfooが100になる
setFoo(100);

// myFooは0のまま
console.log(myFoo);
```

こうした場合、当然ながら変数`myFoo`には「代入を実行した時点での`foo`の値」が入ります。変数`myFoo`は変数`foo`とは無関係ですから、`foo`がどう変化しても`myFoo`の値は変化しません。

この例と対比することでも、「`import { foo } from "./a.mjs";`」が「`const foo = (a.mjsの変数fooの値);`」のような意味**ではない**ことがお分かりになるでしょう。

## モジュール名前空間オブジェクト

`import`文には`import * as mod from "module";`のような構文もあります。これは、モジュールからエクスポートされている変数を全部まとめてオブジェクトにしてインポートするという意味です。この構文によって得られるオブジェクトが**モジュール名前空間オブジェクト**です。長いので以降は名前空間オブジェクトと呼びます。

名前空間オブジェクトが持つ各プロパティは、**インポート元の変数の値を常に反映します**。別の言い方をすれば、名前空間オブジェクトのプロパティがインポート元の変数のエイリアスになっていると言えます。先ほどの`setFoo`の例を少し書き換えることでこれを確かめましょう。

```js
////////// a.mjs
export let foo = 0;

export const setFoo = (value) => {
  foo = value;
}

////////// index.mjs
import * as a from "./a.mjs";

// 0 が表示される
console.log(a.foo);

a.setFoo(100);
// 100 が表示される
console.log(a.foo);
```

`index.mjs`内の変数`a`に`a.mjs`の名前空間オブジェクトが入っています。結果から分かるように、`a.foo`の値は`a.mjs`内の変数`foo`の値を常に反映しています。

名前空間オブジェクトはこの点で特別なオブジェクトです。例えば、次のようにしても再現できません。

```js
import { foo, setFoo } from "./a.mjs";
// これは名前空間オブジェクトの挙動にならない
const a = { foo, setFoo };
```

なぜなら、このように作ったオブジェクトは`a.foo`が「`a`を作った瞬間の変数`foo`の値」になり、変数`foo`の変化に追随しないからです。先ほどの説明の通りこのようにインポートした変数`foo`はインポート元の`foo`のエイリアスですが、それは変数自体の性質であり、「変数`foo`を評価して得た値」は何の変哲のないただの値でしかありません。そのただの値を`a`のプロパティに入れても、名前空間オブジェクトのような挙動にはならないのです。

ちなみに、defaultエクスポートは「`default`という名前でエクスポートされている変数」だったので、名前空間オブジェクトの`default`プロパティとして取得できます。

また、dynamic import（`import("./a.mjs")`）の結果として得られるのもやはり名前空間オブジェクトです。よって、`index.mjs`を次のように書き換えても同じ結果となります。

```js
import("./a.mjs").then(a => {
  // 0 が表示される
  console.log(a.foo);

  a.setFoo(100);

  // 100 が表示される
  console.log(a.foo);
})
```

## なぜ値ではなく変数をエクスポートするのか

ここまで、JavaScriptのモジュールは変数をエクスポートしているのだということを解説しました。しかし、なぜそのような挙動になっているのでしょうか。値をエクスポートした方が単純で分かりやすいような気がします。

その答えは、[ECMAScriptの仕様書がホストされているGitHubリポジトリにひっそりと置かれているFAQ.mdというファイル](https://github.com/tc39/ecma262/blob/master/FAQ.md)にわざわざ書かれています。ちなみに、これは自慢ですが、筆者はECMAScript仕様書に[プルリクエスト](https://github.com/tc39/ecma262/pull/1873)を送ってマージされたことがあります。

このFAQ.mdには、この挙動の理由について次のように書かれています。

> The biggest reason for this is that it allows cyclic module dependencies to work.

つまり、**循環参照があるようなプログラムでも動くようにするため**というのが最大の理由です。

### 循環参照の問題点

まず、そもそも循環参照の何が問題なのかを考えてみましょう。これまで見てきたプログラム例は循環参照がありませんでしたが、その場合モジュールは依存されている側から順番に実行されていました。つまり、`index.mjs`→`a.mjs`→`b.mjs`という依存関係がある場合、まず`b.mjs`が実行され、次に`a.mjs`が実行され、`index.mjs`が実行されました。この実行順序は、**`import`文でインポートした変数には最初から値が入っている**ということを保証するためのものです。先ほどの例を再掲します。

```js
////////// a.mjs
export let foo = 0;

export const setFoo = (value) => {
  foo = value;
}

////////// index.mjs
import { foo, setFoo } from "./a.mjs";

// a.mjs内のfooは0なので0が表示される
console.log(foo);

// a.mjs内のfooが100になる
setFoo(100);

// a.mjs内のfooは100なので100が表示される
console.log(foo);
```

この例では、`a.mjs`が先に実行されて、そのあと`index.mjs`が実行されます。これにより、`index.mjs`の最初の`console.log(foo);`を実行した時点で`foo`にはすでに`0`という値が入っています。ここで`foo`に`0`が入っている理由は、`index.mjs`よりも先に`a.mjs`が実行され、`a.mjs`の中の`export let foo = 0;`が実行されることで`a.mjs`の変数`foo`に`0`が代入されたからなのです。

もし`a.mjs`よりも先に`index.mjs`が実行されていたら、`console.log(foo)`の時点で`foo`にはまだ何も入っていないことになってしまいます。

このように、インポートした変数がすぐ使えることを保証するするために、依存されている側から先に実行するという実行順序になっています。

しかし、循環参照がある場合はこの保証ができなくなります。循環参照とは、例えば`a.mjs`が`b.mjs`をインポートし、`b.mjs`も`a.mjs`をインポートしているというような状態を指します。この場合、どちらを先に実行しても問題が発生してしまいますね。`a.mjs`を先に実行すれば、`a.mjs`は`b.mjs`をインポートしているのに`b.mjs`より`a.mjs`が先に実行されてしまいます。逆でも同じ問題が起きます。

この問題は、実は根本的にはどうしようもありません。JavaScriptでモジュール間の循環参照があった場合、前述の保証は諦めて一定の順番でモジュールを実行します。試しに、循環参照が原因のエラーを発生させてみましょう。

```js
////////// b.mjs
import { varFromA } from "./a.mjs";

export const varFromB = "b";

console.log("varFromA is", varFromA);

////////// a.mjs
import { varFromB } from "./b.mjs";

export const varFromA = "a";

console.log("varFromB is", varFromB);

////////// index.mjs
import "./a.mjs";
```

この例では、`a.mjs`と`b.mjs`が循環参照しています。それぞれが変数をエクスポートし、互いにインポートした変数を利用しています。これを実行すると、次のようなエラーが発生します（Node.js v13.8.0で確認）。

```
console.log("varFromA is", varFromA);
                           ^

ReferenceError: Cannot access 'varFromA' before initialization
```

つまり、`b.mjs`の実行時に、まだ初期化されていない変数`varFromA`を読もうとしたことによるエラーです。

これは、`a.mjs`よりも先に`b.mjs`が実行されたことが原因です。`b.mjs`は`a.mjs`からインポートした変数`varFromA`を評価しましたが、まだ`a.mjs`内で`export const varFromA = "a";`が実行されていないため、変数`varFromA`はまだ初期化されていない変数となっているのです。まだ初期化されていない変数はアクセスすることができません。

ちなみに、「初期化されていない変数」は循環参照に特有の現象ではありません。1ファイル内でも、変数宣言よりも前に変数にアクセスすると同じエラーになります。このことからも先の`varFromA`が`a.mjs`内の`varFromA`に対するエイリアスであることが分かります。

```js
// ReferenceError: Cannot access 'foo' before initialization
console.log(foo);
const foo = 123;
```

なお、変数がまだ初期化されていない区間はTemporal Dead Zone (TDZ) と呼ばれています。`let`や`const`で宣言された変数はその宣言が評価された際に初期化されるためTDZが存在しますが、`var`で宣言された変数は最初から`undefined`に初期化されているため、TDZが存在しません。

### 循環参照でもエラーが発生しない場合

本題に戻ると、ここで重要なのは**TDZにある変数をインポートするだけではエラーにならない**ということです。`import`文により変数のエイリアスができても、それはまだTDZにある変数へのエイリアスを作ったというだけです。そのエイリアスを通じて実際にアクセスしなければエラーは起きないのです。

<!-- 正確にはモジュールの依存関係グラフの解決と環境の初期化はモジュールの実行(Evaluate)段階ではなくその前段階(Link)で行われるため、「TDZにある変数をインポートする」という表現は正確さに欠けるのですが、大目に見ていただきたいところです。 -->

つまり、**TDZにある変数にアクセスしなければ、モジュールが循環参照していてもエラーは起きない**ということになります。そして、実際のところ、モジュールが循環参照しているがTDZにある変数にアクセスしないという例は結構あります。最も典型的なのは、モジュールが関数だけエクスポートしている場合です。先ほどのFAQ.mdに載っている例を引用します。

```js
////////// Even.js
import {isOdd} from "./Odd.js";

export function isEven(num) {
  if (num === 0) {
    return true;
  } else {
    return isOdd(num - 1);
  }
}
////////// Odd.js
import {isEven} from "./Even.js";

export function isOdd(num) {
  if (num === 0) {
    return false;
  } else {
    return isEven(num - 1);
  }
}

////////// main.js
import {isOdd} from "./Odd";

isOdd(2);
```

この例では、`Even.js`と`Odd.js`が循環参照しています。それぞれ、`isEven`と`isOdd`という関数をエクスポートしています。また、お互いにお互いがエクスポートしている関数をインポートし、相互再帰の形で利用しています。

実は、これは循環参照があるにも関わらずエラーが発生しません。その理由は、`Even.js`や`Odd.js`が実行された瞬間はそれぞれ関数をひとつ定義されているだけであり、インポートした変数をすぐに参照するわけではないからです。実際にこれらの関数が実行されるのは`main.js`の中で`isOdd(2)`が実行されたタイミングです。`main.js`は、`Even.js`と`Odd.js`の実行が終わってから実行されます。つまり、`isOdd(2)`が実行されるタイミングではすでに`isOdd`も`isEven`も定義済みである（TDZを抜けている）ということです。これにより、`isOdd`はその中で`isEven`を呼び出すことができ、また`isEven`も`isOdd`を呼び出すことができます。この用意して、循環参照がある状態で関数を定義することができました。

これは、変数がエイリアスとしてインポートされているからこそ実現できることです。この例では`Even.js`→`Odd.js`→`main.js`の順にモジュールが実行されるので、`Even.js`が実行された（`isEven`が初期化された）段階ではまだ`isOdd`は（これは`Odd.js`がエクスポートしている`isOdd`のエイリアスなので）TDZにあります。しかし、`isOdd`は`isEven`の中身から参照されており、この段階で`isOdd`を評価することはないのでTDZによるエラーは起こりません。次に`Odd.js`が実行されたタイミングで`isOdd`が初期化されると、当然ながら`Even.js`から見える`isOdd`も初期化済みになります。このように変数がエイリアスされていることで、インポートした変数が後から初期化されるのでOKというパターンが生まれるのです。

## バンドラによるインポート・エクスポートの扱い

現在のフロントエンド開発では、モジュールを駆使して書かれたプログラムはWebpackに代表されるバンドラによって処理し、インポート・エクスポートの無い単一のプログラムに変換してから実行されます[^note_codesplitting]。つまり、ここまで説明してきたインポート・エクスポートの挙動を実際に処理しているのはバンドラだということです。

[^note_codesplitting]: 実際にはダウンロードサイズの削減を目的として成果物を複数ファイルに分割したりしますが、それは今回の話題とは無関係であることがお分かりだと思います。

ということで、先ほどから出てきているこの例をWebpackでバンドルしたものを見てみましょう。

```js
////////// a.mjs
export let foo = 0;

export const setFoo = (value) => {
  foo = value;
}

////////// index.mjs
import { foo, setFoo } from "./a.mjs";

// a.mjs内のfooは0なので0が表示される
console.log(foo);

// a.mjs内のfooが100になる
setFoo(100);

// a.mjs内のfooは100なので100が表示される
console.log(foo);
```

これを`webpack --mode none`でバンドルしたものから`index.mjs`に相当する部分を抜き出すとこのようになります。


```js:index.mjs
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _a_mjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


// a.mjs内のfooは0なので0が表示される
console.log(_a_mjs__WEBPACK_IMPORTED_MODULE_0__["foo"]);

// a.mjs内のfooが100になる
Object(_a_mjs__WEBPACK_IMPORTED_MODULE_0__["setFoo"])(100);

// a.mjs内のfooは100なので100が表示される
console.log(_a_mjs__WEBPACK_IMPORTED_MODULE_0__["foo"]);
```

最も注目すべき点は、元々のソースコードで変数`foo`を参照していたところが`_a_mjs__WEBPACK_IMPORTED_MODULE_0__["foo"]`というプロパティアクセスに置き換わっている点です。`_a_mjs__WEBPACK_IMPORTED_MODULE_0__`は`a.mjs`の名前空間オブジェクト（をWebpackがエミュレートしているもの）ですね。さすがに「よそのモジュール（＝スコープ外の存在）により変数の中身が勝手に書き換わる」はそのまま実現することができませんが、「よそのモジュールによりオブジェクトのプロパティが勝手に書き換わる」は（よそのモジュールがオブジェクトを参照できれば）実現できそうなので、このような方式が取られています。

次に`a.mjs`に相当する部分はこんな感じです。`__webpack_exports__`というのが多分`a.mjs`の名前空間オブジェクトであり、そのオブジェクトに対して`foo`や`setFoo`というアクセサプロパティを定義しています。これらのプロパティはゲッタを持ち、アクセスされると実際の変数`foo`や`setFoo`の値を返します。

```js:a.mjs
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "foo", function() { return foo; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "setFoo", function() { return setFoo; });
let foo = 0;

const setFoo = (value) => {
  foo = value;
}
```

このことから、Webpackの方針は`import`を頑張って名前空間オブジェクト経由で変換し、名前空間オブジェクトはゲッタを使って再現するというものであることが分かりますね。

## モジュールのアンチパターン

この記事の説明を理解すると、モジュールを書く際に避けるべきパターンが見えてきます。基本的には、「**エイリアスを作れば済むのにわざわざ値を取得してエクスポートしているもの**」がアンチパターンとなります。

### 1. 変数を経由して再エクスポート

変数を再エクスポートしたい場合は、再エクスポート用の構文を使って再エクスポートすることで、変数のエイリアス性を保って再エクスポートできます。途中でローカルスコープの変数を経由させると、エイリアス性が途切れてしまいます。

```js:悪い例
////////// b.mjs
// 変数fooは1秒後に9999になる
export let foo = 1;

setTimeout(()=> { foo = 9999; }, 1000);

////////// a.mjs
import * as b from "./b.mjs";
// fooはa.mjsが実行された瞬間のb.fooの値（1）になる
export const foo = b.foo;

////////// index.mjs
import { foo } from "./a.mjs";

setTimeout(()=> {
  // 2秒後にfooを表示すると1が表示される
  console.log(foo);
}, 2000);
```

この例では、`b.mjs`がエクスポートしている変数`foo`の値が最初は`1`で、1秒後に`9999`に変化します。`a.mjs`は`b.mjs`の`foo`を再エクスポートしているつもりですが、できていません。`a.mjs`のローカル変数として別に`foo`を定義しており、それを`b.foo`の値で初期化されているからです。この`foo`は`b.mjs`の`foo`のエイリアスではなく、初期化時に`b.foo`の値を使っただけでまったく無関係の`foo`だからです。

よって、この例で`index.mjs`を実行すると、2秒後に`1`と表示されます。`index.mjs`がインポートしている`foo`は`a.mjs`の`foo`であり、`b.mjs`の`foo`が変化しても影響されないからです。

`b.mjs`の`foo`の変化が`index.mjs`に伝わってほしければ、正しく再エクスポートしなければいけません。例えば次のようにすれば再エクスポートできます。こう変更すると、`index.mjs`を実行した2秒後に`9999`が表示されます。

```js
////////// a.mjs
import * as b from "./b.mjs";

export { foo } from "./b.mjs";
```

また、次のように「インポートされた変数」を直に`export { }`構文に渡した場合は再エクスポートとして扱ってもらえるので、これでもOKです。こちらの方式だと、`a.mjs`内でも`b.mjs`からインポートした`foo`を参照することができるという利点があります。とにかく、ローカル変数を経由してしまうとだめなのです。

```js
////////// a.mjs
import { foo } from "./b.mjs";

export { foo };
```

### 2. オブジェクトに入れて再エクスポート

別のアンチパターンとして、次のように親切にも「インポートされたものをまとめたオブジェクト」を作ってエクスポートしている場合があります。

```js
////////// utils.mjs
import { someNiceFunc } from "./foo.mjs";
import { otherNiceFunc } from "./bar.mjs";
import { veryUsefulFunc } from "./baz.mjs";

// 変数からエクスポートするパターン
export const utils = {
  someNiceFunc,
  otherNiceFunc,
  veryUsefulFunc,
};

// default exportのパターン
export default {
  someNiceFunc,
  otherNiceFunc,
  veryUsefulFunc,
}
```

これもやはり、変数のエイリアス性が途切れるのでアンチパターンです。変数に入れようがdefaultエクスポートだろうがだめです。例えば、`utils.someNiceFunc`の値はこのモジュールが実行された瞬間の`someNiceFunc`の値であり、その後`someNiceFunc`の値が変化しても追随できません。これは、`utils.mjs`が実行された瞬間に`{ someNiceFunc, otherNiceFunc, veryUsefulFunc }`というオブジェクトリテラルが評価され、その過程で変数`someNiceFunc`の値が参照されているからです。

実際のところ「モジュールからエクスポートされている便利関数が後から変わる」というのは非現実的なシチュエーションですが、これと循環参照を組み合わせると割と現実的な問題となります。

ちょっと長いですがこんな感じの例で考えてみます。

```js
////////// utils.mjs
import { someNiceFunc } from "./foo.mjs";
import { otherNiceFunc } from "./bar.mjs";
import { veryUsefulFunc } from "./baz.mjs";

export default {
  someNiceFunc,
  otherNiceFunc,
  veryUsefulFunc,
}

////////// foo.mjs
export const someNiceFunc = (arg)=> {
    return arg * 2;
}
////////// bar.mjs
import utils from "./utils.mjs";

export const otherNiceFunc = (arg)=> {
    return utils.someNiceFunc(arg) + 1;
}
////////// baz.mjs
import utils from "./utils.mjs";

export const veryUsefulFunc = (arg)=> {
    console.log(utils.otherNiceFunc(arg));
}
////////// index.mjs
import { veryUsefulFunc } from "./baz.mjs";

veryUsefulFunc(100);
```

この例では`foo.mjs`, `bar.mjs`, `baz.mjs`がそれぞれとてもいい感じの関数をエクスポートしており、それらを`utils.mjs`がオブジェクトにまとめています。これらの関数を使いたい場合は`utils.mjs`を経由して使用する想定です。`bar.mjs`や`baz.mjs`も`utils.mjs`経由で他の関数を使用しています。

ところが、そこに行儀の悪い`index.mjs`が現れて、`baz.mjs`から直接`veryUsefulFunc`を読み込んで使用してしまいました。この瞬間にまずい循環参照が発生します。具体的には、`index.mjs`→`baz.mjs`→`utils.mjs`→`bar.mjs`→`utils.mjs`という循環参照の発生により、`baz.mjs`よりも先に`utils.mjs`が実行されます。その結果、`utils.mjs`が実行された段階で`baz.mjs`からエクスポートされている`veryUsefulFunc`はTDZ下にあるため、`utils.mjs`内で以下のエラーが発生してしまいます。

```
ReferenceError: Cannot access 'veryUsefulFunc' before initialization
```

さらに、Webフロントエンド状況が悪化することがあります。フロントエンドではいまだにES5へのトランスパイルが行われることが珍しくなく、その場合`const`は`var`に変換されてTDZが消えます。その結果、上の例は「TDZによるエラーが発生する」という結果の代わりに`utils`が以下のようなオブジェクトになるという結果になります。

```
{
  someNiceFunc: [Function: someNiceFunc],
  otherNiceFunc: [Function: otherNiceFunc],
  veryUsefulFunc: undefined
}
```

つまり、なぜか`utils.veryUsefulFunc`だけ`undefined`になってしまうのです。上記のソースコードからこの結果が得られたとき、あなたは原因を特定することができますか？　ビルドシステムが一因なのでソースコードだけ見ても分からない上に、循環参照が原因なので`import`文の順番を変えたら直るということすらあり得ます。「JavaScriptはクソ言語」と吐き捨てて投げ出したくなる誘惑に負けずにバグの修正までこぎつけることができるでしょうか。

このバグの原因は主に2つあります。ひとつは循環参照を作ったこと、そしてもう一つはインポートしたものをオブジェクトに詰めてエクスポートするというアンチパターンを行なったことです。

後者が100%いつでも避けるべきかは疑問符が付くところですが、避けられるなら避けるべきです。今回の場合は、アンチパターンを避けて`utils.mjs`をこのようにすれば万事解決です。

```js
import { someNiceFunc } from "./foo.mjs";
import { otherNiceFunc } from "./bar.mjs";
import { veryUsefulFunc } from "./baz.mjs";

export {
  someNiceFunc,
  otherNiceFunc,
  veryUsefulFunc,
}
```

それに合わせて`utils.mjs`を使う側も`import * as utils`にするか、あるいは必要なものだけ`utils.mjs`から読み込むようにします。

皆さんもこの先「インポートしたはずのものが`undefined`だ」という謎のバグに出会うことがあるかもしれませんが、その場合は循環参照＋エクスポートのアンチパターンという組み合わせを疑ってみるのも悪くないでしょう。

## まとめ

この記事では、JavaScriptの`import`・`export`文の挙動を「何をエクスポートしているのか」という点を中心に解説しました。とくに、`export`文は常に変数をエクスポートしており、`import`はその変数へのエイリアスを作るのだという点が重要です（`export default`の場合は変数が暗黙に作成されていましたが）。

この挙動は循環参照が発生したときでもなるべくいい感じに動くするようにするためのものですが、その恩恵を得るためには記事で扱ったようなアンチパターンを避ける必要があります。

また、実際にモジュールを使って書かれたコードを処理するのに現在広く使われているwebpackを例にとり、この記事で説明したような挙動が再現されていることを確かめました。

### さらに詳しく知りたい方へ

筆者による以下の記事では、top-level awaitという新たな要素がモジュールシステムに与える影響について解説しています。また、この記事の後半ではECMAScript仕様書を読みながらモジュールの挙動を追っていきます。

モジュールの理解をさらに深いものにしたい方はこちらの記事もぜひご覧ください。

- [top-level awaitがどのようにES Modulesに影響するのか完全に理解する](https://qiita.com/uhyo/items/0e2e9eaa30ec2ff05260)