JavaScriptのexport defaultアンチパターンについて、検証してみた

この記事は[Fringe81 アドベントカレンダー2017](https://qiita.com/advent-calendar/2017/fringe81)の16日目です。

## 始めに
ES6のimport/exportについては
・基本的にexport defaultを使う
・exportする対象が複数あれば、exportを使う
・exportされたモジュールはimportで受け取る
ぐらいの雑な理解をしてました。

「基本的にexport defaultを使う」については、[Design goals for ES6 modules](http://exploringjs.com/es6/ch_modules.html#sec_design-goals-es6-modules)にて、以下のように言及されてます。

> 16.8 Design goals for ES6 modules 
> If you want to make sense of ECMAScript 6 modules, it helps to understand what goals influenced their design. The major ones are:
> ・Default exports are favored
> ・Static module structure
> ・Support for both synchronous and asynchronous loading
> ・Support for cyclic dependencies between modules

> ECMAScript 6 favors the single/default export style, and gives the sweetest syntax to importing the default.
> Importing named exports can and even should be slightly less concise.

ES6は、単一値をexport defaultするスタイルを好む。なるほど〜。

## export defaultは使わないほうがいい？
ところが先月はてブで、[ES modulesのexport defaultは使わないほうがよい](http://gfx.hatenablog.com/entry/2017/11/24/135343#f-6d4ae8a4)という記事タイトルを拝見しました。

あれ？そうだっけ？

「export default推奨」程度の理解しかなかったので、気になって色々調べてみました。（ちなみに先程の記事は、export対象に名前が必須でないため、コード補完と相性が悪いというのが主な内容でした）

色々漁ってみると
[こちらの記事](https://medium.com/@rauschma/note-that-default-exporting-objects-is-usually-an-anti-pattern-if-you-want-to-export-the-cf674423ac38)では、以下の事が言及されてました。

> Note that default-exporting objects is usually an anti-pattern (if you want to export the properties).
> You lose some ES6 module benefits (tree-shaking and faster access to imports).

オブジェクトをexport defaultするのはアンチパターンである（何故なら、ツリーシェイキングやimportへの早いアクセスという観点で、ES6のモジュールの恩恵を受けられない）

へぇ。

でもぶっちゃけると、アンチパターンの理由というか、具体的に何が困るのかしっくり来なかったので
ツリーシェイキング観点で、調査してみました。

## そもそもTree-Shakingとは
簡単に言うと「JSファイルを1つにbundleする際にimportされていないexport文を消す。そしてminifyする際に完全に削除する」機能です。
ES6の静的解析で、使われていない文をジャッジ出来るそうです。詳しくは[Tree-shaking with webpack 2 and Babel 6](http://2ality.com/2015/12/webpack-tree-shaking.html)にて。

## Tree-Shakingを試してみた
webpackとbabelを使って、Tree-Shakingの機能を確認してみました。webpack.configとbabelrcは以下のように設定。

```.js
// webpack.config.js
const path = require('path');
module.exports = {
  entry: {
    app: ['./entry.js']
  },
  output: {
    path: path.join(__dirname, 'build'),
    filename: '[name].bundle.js'
  },
  devtool: '#source-map',
  module: {
    loaders: [
      {loader: 'babel-loader', exclude: /node_modules/, test: /\.js$/}
    ]
  },
  resolve: {
    extensions: ['.js']
  }
};

// .bablerc
{
  "presets": ["es2015"]
}
```

早速、以下のサンプルコードをwebpackでbundleします。

```.js
// exportA.js
export function foo() {
  return 'foo';
}
export function bar() {
  return 'bar';
}

// entry.js
import {foo, bar} from './exportA';
console.log(foo(), bar());

//app.bundle.js
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.foo = foo;
exports.bar = bar;
function foo() {
  return 'foo';
}
function bar() {
  return 'bar';
}
```

fooとbarが両方exportsされてます。
では、サンプルコードからbarを使わないように修正して再度出力します。

```.js
// entry.js
import {foo, bar} from './exportA';
console.log(foo());

// app.bundle.js
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.foo = foo;
exports.bar = bar;
function foo() {
  return 'foo';
}
function bar() {
  return 'bar';
}
```

あれ？barもexportされてるやん！


## ESmoduleからcommonJSへの変換を無効にする必要あり
webpackのpluginでbabel-loaderを使っている場合、ESモジュールがcommonJSに変換されてしまい、使われていないexport文が削除されなくなるそうです（ここは正直あまりわかっておらず）、詳しくは[Webpack2のTree Shakingを試す](https://qiita.com/pirosikick/items/863830856891d40308cb)にて。

commonJSへの変換を無効化するため、babelrcを以下のように修正して、再チャレンジ。

```.js
// .babelrc
{
  "presets": [["es2015", {"modules": false}]]
}

// entry.js
import {foo, bar} from './exportA';
console.log(foo(), bar());

// app.bundle.js
"use strict";
/* harmony export (immutable) */ __webpack_exports__["b"] = foo;
/* harmony export (immutable) */ __webpack_exports__["a"] = bar;
function foo() {
  return 'foo';
}
function bar() {
  return 'bar';
}
```

bundleファイルの内容が変わりましたね。先程の設定では `exports.XXX` でしたが、今回はexport対象は `__webpack_exports__` に格納されてるようです。
では、サンプルコードからbarを消します。

```.js
// entry.js
import {foo, bar} from './exportA';
console.log(foo());

// app.bundle.js
"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = foo;
/* unused harmony export bar */
function foo() {
  return 'foo';
}
function bar() {
  return 'bar';
}
```

おおーー！
`__webpack_exports__`に`bar`が格納されなくなりましたね！
代わりに`unused harmony export bar`としてコメントアウトされてますね。

ちなみにbundleファイルにはbar関数の定義が残っておりますが、minifyすると完全にbarという文字列が削除されます。（確認程度のためだったので、[Minify your Javascript](https://javascript-minifier.com/)でminifyしました。）

## export defaultでTree-Shakingしてみる
Tree-Shakingの機能自体はわかったので、オブジェクトをexport defaultし、Tree-Shakingの恩恵を受けられないかどうか、確認してみましょう。

## export default オブジェクト
以下、サンプルコードその2。

```.js
// defaultA.js
export default {
  foo: function() {
    return 'foo';
  },
  bar: function() {
    return 'bar';
  }
}

// entry.js
import TestFunc from './defaultA';
console.log(TestFunc.foo(), TestFunc.bar());

// app.bundle.js
"use strict";
/* harmony default export */ __webpack_exports__["a"] = ({
  foo: function foo() {
    return 'foo';
  },
  bar: function bar() {
    return 'bar';
  }
});
```

なるほど。`__webpack_exports__`には、foo、barをプロパティとして持つオブジェクトが丸ごと格納されてますね。
では、サンプルコード2からTestFuncを消して再度出力。

```.js
// entry.js
import TestFunc from './defaultA';
console.log(TestFunc.foo());

// app.bundle.js
"use strict";
/* harmony default export */ __webpack_exports__["a"] = ({
  foo: function foo() {
    return 'foo';
  },
  bar: function bar() {
    return 'bar';
  }
});
```

なるほどー。bar()はこのタイミングでは削除されないんですね。
export foo() export bar()の時は、fooとbarをそれぞれexportしていたので、未使用のbarがexportされなくなったのですが
export defaultの場合、fooもbarもプロパティとして1つのオブジェクトがexportされるので、消えないのですかね。

ちなみに **minifyしても`bar`の文字列は消えませんでした。**

では、fooもbarも両方消してみましょう。

```.js
// entry.js
import TestFunc from './defaultA';

// app.bundle.js
"use strict";
/* unused harmony default export */ var _unused_webpack_default_export = ({
  foo: function foo() {
    return 'foo';
  },
  bar: function bar() {
    return 'bar';
  }
});
```

おおお。`__webpack_exports__`に何も格納されず、 `_unused_webpack_default_export` にexportされたオブジェクトが格納されてますね！ `_unused_webpack_default_export` はコード内でどこにも使われておりません。
unsedに格納されているからかと思いますが、minifyするとfooもbarも完全に消えました。

## export default class
classでも一応確認してみました。（Reactのコンポーネントはexport default classをよく使うため）
以下サンプルコード3。

```.js
// defaultB.js
export default class {
  constructor() {
    this.foo = function() {
      return 'foo';
    };
    this.bar = function() {
      return 'bar';
    };
  }
}

// entry.js
import TestClass from './defaultB';
const t = new TestClass();
console.log(t.foo(), t.bar());

// app.bundle.js
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var _class = function _class() {
  _classCallCheck(this, _class);

  this.foo = function () {
    return 'foo';
  };
  this.bar = function () {
    return 'bar';
  };
};
/* harmony default export */ __webpack_exports__["a"] = (_class);
```

予想通り、`__webpack_exports__` にclassが丸ごと格納されてます。
では、TestClassを削除して再チャレンジ。

```.js
// entry.js
import TestClass from './defaultB';

// app.bundle.js
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var _class = function _class() {
  _classCallCheck(this, _class);

  this.foo = function () {
    return 'foo';
  };
  this.bar = function () {
    return 'bar';
  };
};

/* unused harmony default export */ var _unused_webpack_default_export = (_class);

```

勿論でしたが、classはオブジェクトと同じ動きでした。

## export default プリミティブ型

オブジェクト
適当な文字列をexport defaultしてみよう。以下サンプルコード4。

```.js
// defaultC.js
const Foo = 'foo';
export default Foo

// entry.js
import Foo from './defaultC';
console.log(Foo);

// app.bundle.js
"use strict";
var Foo = 'foo';
/* harmony default export */ __webpack_exports__["a"] = (Foo);
```

では、Fooを削除しましょう。

```.js
// entry.js
import Foo from './defaultC';

// app.bundle.js
"use strict";
var Foo = 'foo';
/* unused harmony default export */ var _unused_webpack_default_export = (Foo);

```

お！？
プリミティブ型でも、`_unused_webpack_default_export` に格納されるんですね（minifyすると削除されました。）

ああーー、なるほど。
どういう型であれ、import側で使われるものは `__webpack_exports__` に格納され、使われないものは `_unused_webpack_default_export`に格納されると。（プリミティブ型は、bundleのタイミングで消されるとか思っちゃってました）

## 考察/まとめ
Tree-Shakingの観点だと以下のことが言えるのかな〜と思いました。
・export defaultの対象は「単一値」であるもの（プリミティブ型や、クラス型）が推奨される。
・export defaultの対象が、複数のプロパティを持ち得るオブジェクトの場合、importで使われないプロパティも、exportされるし、minifyしても消えない。何故ならオブジェクト丸ごとexportするため。
・一方でexportA/exportBでは、個々をexportしているため、impotで使われないプロパティはexportされない。
・bundle.jsの容量が大きいことが、何かしらのボトルネックになる場合、export defaultオブジェクトはやめた方がいいかも。


以上です。ありがとうございました！