モダン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
を用いた次の例ではどうでしょう。
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に流し込んでみると、以下のようなコードが吐き出されます。
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()を使用します。
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を使用してみましょう。
(なにを血迷ってもオレオレ実装するのはやめましょう。)
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-js
とregenerator runtime
を読み込んでいます。)
まとめ
- BabelはES6+からES5へのトランスパイルを**「構文変換+Polyfill」**によって実現している
- 構文変換は事前処理。Polyfillは実行時処理。
- トランスパイルはコード量を代償に、ネイティブサポートされていない記述も使用できるようにしている
- サポート対象のブラウザが決まっているのであれば、Env presetを用いて変換が最小限になるようにすることでコードの肥大化を抑えましょう
- Polyfillはブラウザネイティブと同等の動作を再現しているだけで、パフォーマンス劣化や動作再現不可能な機能があったりすることを忘れない
- グローバル汚染問題もあるが、これは
Runtime transform
を用いてPolyfill対象部分のコードをまるっと事前に置換してやることで回避することができる
- グローバル汚染問題もあるが、これは
おまけ: Babelによる構文変換を眺める
ES2015(ES6)記法をBabelのREPLに流し込んでみて、どのように構文変換が行われているか見てみましょう。
もしかすると、Babelによる出力結果を見たこと無いって人も多いのでは?
let/constによるブロックスコープ
{
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
テンプレート文字列
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
改行記号も自動でいれてくれている
分割代入
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
スプレッド演算子
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"]