LoginSignup
113

More than 5 years have passed since last update.

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

Posted at

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

また、今はBabelという便利なものがあるので、とりあえず

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

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

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

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

そもそもBabelってなんだっけ

Babelの公式サイトによると、以下のようなことが書かれています。

・ 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対応状況を見てもらえれば分かるのですが、Babelはcore-jsというブラウザ標準APIのPolyfillと組み合わせることでES2015(ES6)の構文への対応を行っています。
(これでも100%対応しているわけではないということを知っておきましょう。)

Learn ES2015を見れば、ES2015のなにが構文変換でなにがPolyfillで対応されているかがわかります。

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

構文変換をみてみる

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

百聞は一見にしかずということで、実際のコードをBabelのREPLを使いどのような変換が行われるかを見てみましょう。

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

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

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

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

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

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

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

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

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に流し込んでみると、以下のようなコードが吐き出されます。

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の対応を見てみると、

Support via polyfill

と記載があります。

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

早速、簡単な例で見てみましょう。
今回はES2015より導入されたArray.prototype.find()を使用します。

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()のサポートを見てみましょう。

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

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

通常であればcore-jsなどのPolyfillのライブラリを使用するのですが、今回はわかりやすさのためにMDNに書かれているPolyfillを使用してみましょう。
(なにを血迷ってもオレオレ実装するのはやめましょう。)

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というプラグインが用意されているので、バベラーの方はこちらを使用してあげるのがよいでしょう。
(babel-polyfillも内部的にはcore-jsregenerator runtimeを読み込んでいます。)

まとめ

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

おまけ: Babelによる構文変換を眺める

ES2015(ES6)記法をBabelのREPLに流し込んでみて、どのように構文変換が行われているか見てみましょう。
もしかすると、Babelによる出力結果を見たこと無いって人も多いのでは?

let/constによるブロックスコープ

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
トランスパイル後
"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

テンプレート文字列

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
トランスパイル後
"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

改行記号も自動でいれてくれている

分割代入

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
トランスパイル後
"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

スプレッド演算子

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"]
トランスパイル後
"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"]

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
113