Babelによるトランスパイルの挙動について理解する

モダンJavascriptの世界において、過去とうまく付き合っていくためには`トランスパイル`や`Polyfill`といったものから逃れることは不可能といっても過言ではないでしょう。
(まだjQuery全盛の世界線で時が止まっている方は、[「イマドキのJavaScriptの書き方2018」](https://qiita.com/shibukawa/items/19ab5c381bbb2e09d0d9)などを今すぐ読んでください。お願いします。)

また、今は[`Babel`](https://babeljs.io/)という便利なものがあるので、とりあえず

```shell
npm install --save-dev babel-core babel-polyfill babel-preset-env
```

みたいに入れてしまえば、細かいことを気にすることなくいい感じに、古いブラウザでも動くコードを生成できる環境を用意できるようになりました。

ただ、Babelでとりあえず変換すれば動作してしまうが故に、細かい設定を見直さなかったり、そもそもどういった処理がなされているのかを理解していなかったり、ということも多々あるように思えます。(あくまで自分調べ)

ので、今回は
「Babelのトランスパイルがどのように行われているのか」
という流れを追いつつ、周辺知識を拾っていく感じで話をしていくことにします。

## そもそもBabelってなんだっけ
[Babelの公式サイト](https://babeljs.io/)によると、以下のようなことが書かれています。

> ・ Babel is a JavaScript compiler.
> ・ Babel has support for the latest version of JavaScript through syntax transformers.
> ・ Since Babel only transforms syntax (like arrow functions) ~ (略)

つまり、Babelは
**「JavaScriptの構文変換を行うコンパイラ(∈トランスパイラ)」**
だといえますね。

ここで注目して欲しいのは、**あくまでBabel自体(`babel-core`)は構文変換しか行わない**ということです。

## え、どういうこと？
[BabelのES2015対応状況](http://kangax.github.io/compat-table/es6/#babel)を見てもらえれば分かるのですが、Babelは`core-js`というブラウザ標準APIの`Polyfill`と組み合わせることでES2015(ES6)の構文への対応を行っています。
(これでも100%対応しているわけではないということを知っておきましょう。)

[Learn ES2015](https://babeljs.io/learn-es2015/)を見れば、ES2015のなにが構文変換でなにが`Polyfill`で対応されているかがわかります。

つまりBabelによるトランスパイルの動作は、
**「構文変換+`Polyfill`」**
によって実現されています。

## 構文変換をみてみる
構文変換は、Babelによる変換のタイミングで行われる**事前処理**になります。

百聞は一見にしかずということで、実際のコードを[BabelのREPL](http://babeljs.io/repl/)を使いどのような変換が行われるかを見てみましょう。

ここで、ES2015の構文で書かれた、以下のコードがあったとします。

```js:アロー関数を用いた二乗計算
const squared = num => num * num;

let result = squared(5);
console.log(result); // 25
```

これをES5環境でも動くようにしたいので、早速Babelで変換してみます。

```js:アロー関数を用いない二乗計算
var squared = function squared(num) {
  return num * num;
};

var result = squared(5);
console.log(result); // 25
```

すると、上記のような変換前と同等の動作をする構文へと変換が行われます。

では次にアロー関数と同様、ES2015から導入された`Promise`を用いた次の例ではどうでしょう。

```js:Promiseを用いた非同期処理
var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise
  .then(value => {
    console.log(value); // 1
    return value + 1;
  })
  .then(value => {
    console.log(value); // 2
    return value + 1;
  })
  .then(value => {
  console.log(value);  // 3
});
```

これをBabelに流し込んでみると、以下のようなコードが吐き出されます。

```js:Babelによる変換後
var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise
  .then(function(value) {
    console.log(value); // 1
    return value + 1;
  })
  .then(function(value) {
    console.log(value); // 2
    return value + 1;
  })
  .then(function(value) {
    console.log(value); // 3
  });
```

おやおや。
確かにアロー関数部分の構文変換は行われていますが、肝心の`Promise`はそのままですね。
これだと`Promise`が実装されていないブラウザ(ES5までしかサポートされていない環境)では動作しませんね。

こういった場合の解決策として使われるのが先述した`Polyfill`です。

## Polyfillをみてみる
ここで、Babelにおける[Promise](https://babeljs.io/learn-es2015/#promises)の対応を見てみると、

> Support via polyfill

と記載があります。

`Polyfill`とは
「ブラウザネイティブの実装と同等の動作をするように、JavaScriptで再現したもの」
で、**コード実行時**にサポート状況に応じて適応させるという処理になります。

早速、簡単な例で見てみましょう。
今回はES2015より導入された[Array.prototype.find()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/find)を使用します。

```js:Array.prototype.find()
var users = [
  { name: "Azu", score: 42 },
  { name: "Jumpei", score: 100 },
  { name: "Nanaka", score: 81 },
  { name: "Rina", score: 94 },
  { name: "Wataru", score: 37 },
  { name: "Reika", score: 55 }
];

var isPerfectScore = function isPerfectScore(user) {
  return user.score === 100;
};

// Array.prototype.find()
let perfectScoreUser = users.find(isPerfectScore);
if (perfectScoreUser) {
  console.log(perfectScoreUser.name); // Jumpei
}
```

上記のようなコードを実装した場合、ES2015が動く環境でないと実行することはできません。
ES2015が出て幾ばくかの時が経った今、まさかそんなブラウザはないでしょうとお思いの方もいらっしゃるかもしれません。
そんな方は、[Array.prototype.find()のサポート](http://kangax.github.io/compat-table/es6/#test-Array.prototype_methods_Array.prototype.find_a_href=_https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find_title=_MDN_documentation_img_src=_../mdn.png_alt=_MDN_(Mozilla_Development_Network)_logo_width=_15_height=_13_/_/a_nbsp;)を見てみましょう。

~~案の定IE11は対応していませんね。~~

IEなんてばっさりと切ってもいいぞという分かり手がいれば良いのですが、現実はそう甘くないことが多いでしょう。
そんな場合には、`Array.prototype.find()`の`Polyfill`を使うことで解決することが出来ます。

通常であれば[`core-js`](https://github.com/zloirock/core-js)などの`Polyfill`のライブラリを使用するのですが、今回はわかりやすさのために[MDNに書かれているPolyfill](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/find#Polyfill)を使用してみましょう。
(なにを血迷ってもオレオレ実装するのはやめましょう。)

```js:Array.prototype.find()+polyfill
var users = [
  { name: "Azu", score: 42 },
  { name: "Jumpei", score: 100 },
  { name: "Nanaka", score: 81 },
  { name: "Rina", score: 94 },
  { name: "Wataru", score: 37 },
  { name: "Reika", score: 55 }
];

var isPerfectScore = function isPerfectScore(user) {
  return user.score === 100;
};

// Polyfill
if (!Array.prototype.find) {
  Array.prototype.find = function(predicate) {
    if (this === null) {
      throw new TypeError('Array.prototype.find called on null or undefined');
    }
    if (typeof predicate !== 'function') {
      throw new TypeError('predicate must be a function');
    }
    var list = Object(this);
    var length = list.length >>> 0;
    var thisArg = arguments[1];
    var value;

    for (var i = 0; i < length; i++) {
      value = list[i];
      if (predicate.call(thisArg, value, i, list)) {
        return value;
      }
    }
    return undefined;
  };
}

// Array.prototype.find()を実行
let perfectScoreUser = users.find(isPerfectScore);
if (perfectScoreUser) {
  console.log(perfectScoreUser.name); // Jumpei
}
```

このような実装にすることで、

- ブラウザネイティブでサポートされていればブラウザネイティブのAPIを使用
- サポートされていない場合は`Polyfill`による実装を使用

というように動作させることが出来るので、ブラウザのサポート状況を気にすることなくメインロジックの実装をすることができますね。

Babelには[`babel-polyfill`](https://babeljs.io/docs/usage/polyfill/)というプラグインが用意されているので、バベラーの方はこちらを使用してあげるのがよいでしょう。
(`babel-polyfill`も内部的には[`core-js`](https://github.com/zloirock/core-js)と[`regenerator runtime`](https://github.com/facebook/regenerator/tree/master/packages/regenerator-runtime)を読み込んでいます。)

## まとめ
- BabelはES6+からES5へのトランスパイルを**「構文変換+Polyfill」**によって実現している
    - 構文変換は事前処理。Polyfillは実行時処理。
- トランスパイルは**コード量を代償に**、ネイティブサポートされていない記述も使用できるようにしている
    - サポート対象のブラウザが決まっているのであれば、[Env preset](https://babeljs.io/docs/plugins/preset-env)を用いて変換が最小限になるようにすることでコードの肥大化を抑えましょう
- Polyfillはブラウザネイティブと同等の動作を再現しているだけで、パフォーマンス劣化や動作再現不可能な機能があったりすることを忘れない
    - グローバル汚染問題もあるが、これは[`Runtime transform`](https://babeljs.io/docs/plugins/transform-runtime)を用いてPolyfill対象部分のコードをまるっと事前に置換してやることで回避することができる

## おまけ: Babelによる構文変換を眺める
ES2015(ES6)記法を[BabelのREPL](http://babeljs.io/repl/)に流し込んでみて、どのように構文変換が行われているか見てみましょう。
もしかすると、Babelによる出力結果を見たこと無いって人も多いのでは？

### let/constによるブロックスコープ
```js:ES2015(ES6)記法
{
  let scope1 = "hoge";
  
  {
      const scope2 = "fuga"
      
    {
    	var scope3 = "piyo";
    }
    
    console.log(scope1);  // hoge
    console.log(scope2);  // fuga
    console.log(scope3);  // piyo
    
  }
  
  console.log(scope1);  // hoge
  console.log(scope2);  // ERROR: scope2 is not defined
  console.log(scope3); // piyo
}

console.log(scope1);  // ERROR: scope1 is not defined
console.log(scope2);  // ERROR: scope2 is not defined
console.log(scope3); // piyo
```

```js:トランスパイル後
"use strict";

{
  var _scope = "hoge";

  {
    var _scope2 = "fuga";

    {
      var scope3 = "piyo";
    }

    console.log(_scope); // hoge
    console.log(_scope2); // fuga
    console.log(scope3); // piyo
  }

  console.log(_scope); // hoge
  console.log(scope2); // ERROR: scope2 is not defined
  console.log(scope3); // piyo
}

console.log(scope1); // ERROR: scope1 is not defined
console.log(scope2); // ERROR: scope2 is not defined
console.log(scope3); // piyo
```

### テンプレート文字列
```js:ES2015(ES6)記法
const num1 = 5;
const num2 = 10;

console.log(`num1 = ${num1}
num2 = ${num2}
num1 * num2 = ${num1 * num2}
`);
// num1 = 5
// num2 = 10
// num1 * num2 = 50
```

```js:トランスパイル後
"use strict";

var num1 = 5;
var num2 = 10;

console.log(
  "num1 = " +
    num1 +
    "\nnum2 = " +
    num2 +
    "\nnum1 * num2 = " +
    num1 * num2 +
    "\n"
);
// num1 = 5
// num2 = 10
// num1 * num2 = 50
```
改行記号も自動でいれてくれている

### 分割代入
```js:ES2015(ES6)記法
let [fruit1, fruit2] = ["apple", "banana"];
console.log(`fruit1: ${fruit1}, fruit2: ${fruit2}`); 
// fruit1: apple, fruit2: banana

// swap
[fruit1, fruit2] = [fruit2, fruit1];
console.log(`fruit1: ${fruit1}, fruit2: ${fruit2}`); 
// fruit1: banana, fruit2: apple
```

```js:トランスパイル後
"use strict";

var fruit1 = "apple",
  fruit2 = "banana";

console.log("fruit1: " + fruit1 + ", fruit2: " + fruit2);
// fruit1: apple, fruit2: banana

// swap
var _ref = [fruit2, fruit1];
fruit1 = _ref[0];
fruit2 = _ref[1];

console.log("fruit1: " + fruit1 + ", fruit2: " + fruit2);
// fruit1: banana, fruit2: apple
```

### スプレッド演算子
```js:ES2015(ES6)記法
const [shopName, ...fruit] = ['Fruit Store', 'kiwi', 'cherry', 'peach'];
console.log(shopName); // Fruit Store
console.log(fruit); // ["kiwi", "cherry", "peach"]

const fruitList = ['apple', 'banana',  ...fruit];
console.log(fruitList); // ["apple", "banana", "kiwi", "cherry", "peach"]
```

```js:トランスパイル後
"use strict";

function _toConsumableArray(arr) {
  if (Array.isArray(arr)) {
    for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
      arr2[i] = arr[i];
    }
    return arr2;
  } else {
    return Array.from(arr);
  }
}

var shopName = "Fruit Store",
  fruit = ["kiwi", "cherry", "peach"];

console.log(shopName); // Fruit Store
console.log(fruit); // ["kiwi", "cherry", "peach"]

var fruitList = ["apple", "banana"].concat(_toConsumableArray(fruit));
console.log(fruitList); // ["apple", "banana", "kiwi", "cherry", "peach"]
```