traceur-compiler 入門
最近ちょっとはまってるtraceur-compiler
について紹介していきます。
前書き
今回、書いてたら非常に長くなってしまって、ちょっとした薄い本の記事くらいの文量があります。
その代わり、公式ドキュメント + コードの中を読みこんで書いているので、今のところ多分日本では一番詳しい記事かと思います。
すごく長いので章分けしました。興味が有るところだけ読み飛ばしてもらってもいいかと思います。
第一章 traceur-compiler概要
traceur-compilerとは
Googleが作っている EcmaScript6 形式で書かれた JavaScript を EcmaScript5 の形式に変換してくれるツールです。"トレーサーコンパイラー"と読みます。
最初にその名前を聞いたのは 2011年の NodeConf の発表でした。3年の後に多くのES6の機能をサポートし、次のAngularJSではES6 + traceur compilerで書かれると発表されるなど、大きなプロダクトで採用されようとしています。
ES6の文法や新機能をフルサポートしたブラウザは現時点では存在しませんが、traceur-compilerがES5の形式に変換してくれることで対応していないブラウザでも実行できるようになるという嬉しい機能を持っています。
また、traceur-compilerの機能の一つですが、traceur-compilerのランタイムエンジンを使うことでアクセス時にES5に変換してくれるという機能をもっているため、直接ES6で書いて、ブラウザに評価させることができます。
traceur-compilerで何が嬉しいのか
ES6は仕様fixが来年6月に延長されたとはいえ、盛り上がりを見せています。
Chromeの開発ビルド(Canary)やFireFoxの開発版(Aurora)にも多くのES6機能追従がされてますし、一部の機能は既に今のブラウザでも扱えるようになっています。
またJavaScriptには他の言語には必ずあるような一般的な機能が欠落している事も多く、それをカバーするためにUnderscoreといったライブラリやCoffeeScript, TypeScript, JSXといったaltJSが台頭しています。
ES6に対応したJavaScirptを使うことで、ライブラリの機能が不要になり、依存ライブラリを減らすことができたり、altJSに頼らなくても豊富な言語機能が扱える可能性が広がります。
また数年後にはES6が広まっていることを考えるとその時までにES6の新文法、新機能に慣れておいたほうがスムーズな移行が期待できます。
また、最終的にはtraceur-compilerはES6が広まることで捨てることができる、という点が他のaltJSやライブラリ群と比較して最も優れていて嬉しい点だと思います。
Getting Started
何はともあれ始めてみましょう。traceur を使ってES6を活用するにはオンラインでES6を直接実行する場合と、事前にオフラインでES6からES5に変換してから構文を実行させる2通りの方法があります。
オンラインでES6を実行する
とりあえずES6のコードを動かすだけなら以下のようにtraceur.js
とbootstrap.js
を読み込むだけで実現可能です。
以下の例では、ES6にあるクラス構文とblocking scope(let)を扱った例を紹介します。
<!DOCTYPE html> <html> <head> <title>Hello World!</title> </head> <body> <h1 id="message"></h1> <!-- traceurのランタイムエンジンを読み込む --> <script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script> <script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script> <!-- letはまだtraceurとしてはexperimentalな機能なのでexperimentalフラグを立てる --> <script> traceur.options.experimental = true; </script> <script type="module"> // Greeterというモジュールを作り、読み込むとh1タグにHello Worldと出るようにする // classが定義できる class Greeter { // constructorメソッドを作るとnewの時に読み込まれる constructor(message) { this.message = message; } // greetメソッド greet() { // letという新しい変数定義 // letを使うとblocking scopeといってこのblock内でしか有効じゃない let element = document.querySelector('#message'); element.innerHTML = this.message; } } let greeter = new Greeter('Hello, world!'); greeter.greet(); </script> </body> </html>
これだけで実行すると以下のような感じになります*1。
さて、オンラインで実行するのはデモには良いのですが、毎回アクセスする度にES6からES5への変換が内部で走るのでそのオーバーヘッドがかかることになり、非効率的です。効率的に実行するならトランスパイル済みのものを配布する方がよいでしょう。
コマンドからES5にトランスパイルする
というわけで事前にトランスパイルを行いましょう。
事前にトランスパイルするならtraceurコマンドが必要になります。
$ npm install traceur -g
さっきの例とはちょっと違ってES6のgenerator関数を使った例でfibonacci数列を出してみましょう。
// fibonacci関数をgenerator関数を使って書く // generator関数はfunction*で定義する function* fibonacci(){ // ES6の場合変数定義の基本はletにした方が良くなるはず... let fn1 = 1; let fn2 = 1; for (;;){ let current = fn2; fn2 = fn1; fn1 = fn1 + current; // yieldで足しあわせた数を返す yield current; } } // for-ofによる繰り返し、generator関数はiterableなオブジェクトを返すので、 // iterableなオブジェクトを繰り返すには for-of文を使う for (let seq of fibonacci()) { // 1000までいったらbreak, つまり1000までの数しかfibonacci数列の表示を行わない if (seq > 1000) { break; } // #fibタグ以下に書く let element = document.querySelector('#fib'); let p = document.createElement("p"); p.innerHTML = seq; element.appendChild(p); }
これを適当に fib.js
とかにして保存したら、以下の方法でtraceurコマンドを実行してください。
$ traceur --out [出力先] --script [入力元]
# experimentalオプションをオンにする $ traceur --experimental --out dist/fib.js --script fib.js
こうしておくと、dist/fib.js
にトランスパイルした結果を出力してくれます。
ただ、トランスパイルしたとしてもtraceurの実行時にtraceurのruntimeスクリプト(traceur-runtime.js)は必要になります。
なので、htmlは以下のようになります。
<!DOCTYPE html> <html> <body> <div id="fib"></div> <!-- traceurのruntimeスクリプトが必要 --> <script src="https://google.github.io/traceur-compiler/bin/traceur-runtime.js"></script> <script src="dist/fib.js"></script> </body> </html>
generator/yield/for-ofを使ったES6の構文でフィボナッチ数列が出ていることが分かるかと思います。
このtraceurを使うことでgeneratorやyieldやlet、for-ofといった構文でもES5の構文にトランスパイルされます。
トランスパイル後のファイルがどうなってるかに関しては次章以降の話にします。
traceur コマンドで逐次実行する
トランスパイルするだけじゃなくて、traceurコマンドを実行することでその場でスクリプトを評価することができます。
$ traceur [実行したいスクリプトへのパス]
$ traceur es6.js
第一章まとめ
- traceur-compilerの概要説明
- Getting Started
- REPL等の便利ツール
第二章 ES6言語仕様概要
traceurが提供する言語機能一覧
traceurがサポートしているES6の機能一覧を紹介していきます。実はtraceurはES6だけじゃなくてまだproposalでしかないような言語の機能も提供しています。この部分を読んでおくとtraceurが今提供しているものが分かるので、traceurからES6を学ぶ人は抑えておくとよいでしょう。
本章の説明は長いので、ES6の内容を既に知っている人は読み飛ばして第三章の話を読んだほうが良いです。
Array Comprehension
いわゆるArray内包表記というやつですね。配列の中でforを使って演算させる事が可能です。
具体的には以下のように書くことが可能です。
var array = [for (x of [0, 1, 2]) for (y of [0, 1, 2]) x + '' + y]; console.log(array); // '00', '01', '02', '10', '11', '12', '20', '21', '22'
これがあると配列の初期化時に柔軟にコードを実行できるようになります。
Arrow Function
アロー関数、CoffeeScriptとかだとよく見ますね。
// (x) => {} で関数定義になる // 今までだと var square = function(x) { return x * x; }; var square = (x) => { return x * x; }; console.log(square(4)); //16 // 引数が一つならカッコを省略できるし、returnの一行しかbodyがないならreturnも省略可能 var square2 = x => x * x; console.log(square2(4)); //16 // 引数を受け取りvalueプロパティを持つオブジェクトに格納してreturnする関数 var objectify = x => ({ value: x }); console.log(objectify(4)); //{ value:4 }
アロー関数は先日のjs.nextでも話題になってましたね。chromeのcanaryでの実装は始まったようですが、stableで有効になるのはもう少し先でしょう。
Classes
第一章の例でも登場したクラス。
クラスと継承がサポートされたことで以下のように書くことが可能です。
// Characterクラス class Character { constructor(x, y) { this.x = x; this.y = y; this.health_ = 100; } attack(character) { character.health_ -= 10; } } // 当然継承もある。 // Monsterクラスに継承 class Monster extends Character { constructor(x, y, name) { super(x, y); this.name = name; } // メソッド書くときはこう書く attack(character) { // 親クラスのメソッド呼ぶときはこう super.attack(character); // super(character)でも同じ意味になる } // get prefixを付けられる get isAlive() { return this.health_ > 0; } get health() { return this.health_; } // set prefixを付けられる set health(value) { if (value < 0) throw new Error('Health must be non-negative.'); this.health_ = value; } } var myMonster = new Monster(5,1, 'arrrg'); var yourMonster = new Monster(5,1, 'nyan'); // get prefixをつけるとプロパティアクセスのようにメソッドを扱える console.log(myMonster.health); // 100 console.log(myMonster.isAlive); // true // set prefixでも同様。 myMonster.health = 1; console.log(myMonster.health); // 1 console.log(myMonster.isAlive); // true myMonster.attack(yourMonster); console.log(yourMonster.health); //90
Computed Property Names
プロパティのキーに演算ができるようになりました。
var x = 0; var obj = { [x] : 'hello', [x+1] : 'world' }; console.log(obj); // {0: 'hello', 1:'world'}
Default Parameter
引数にデフォルトのパラメータを取ることができるようになりました。
function f(list, indexA = 0, indexB = list.length) { return [list, indexA, indexB]; } console.log(f([1,2,3]); // [1,2,3], 0, 3 console.log(f([1,2,3],1); // [1,2,3], 1, 3 console.log(f([1,2,3],1,2); // [1,2,3], 1, 2
Destructuring Assignment
デストラクチャリング、和訳すると分配束縛と呼ばれる機能です。Clojureにある機能ですね。
これを利用すると配列やオブジェクトで設定した値を取り出しやすくなります。
一番良く使うのは値をswapさせる時かと思います。
具体的には以下のとおり。
var hoge = 123; var fuga =456; // 値をswapする var [fuga, hoge] = [hoge, fuga]; console.log(hoge); // 456 console.log(fuga); // 123 var [a, [b], [c], d] = ['hello', [', ', 'junk'], ['world']]; console.log(a + b + c); //hello, world (aに"hello", bに",", cに"world"が入ってる ) var pt = {x: 123, y: 444}; var {x, y} = pt; console.log(x, y); // 123 444
Iterators and For Of
Iterableなオブジェクトを作れるようになり、それをiterateさせるためにFor-Of構文を使うと綺麗に書けます。
var res = []; for (var element of [1, 2, 3]) { res.push(element * element); } console.log(res); // [1, 4, 9]
Iterableなオブジェクトを作るにはSymbol.Iteratorを使います。toStringのiterate版、いわゆるtoIterateみたいなものだと思ってください。
// 1000までの値を返すfibonacciを作る var fibonacci = { // Symbol.iteratorを持つメソッドを持つオブジェクトにする [Symbol.iterator]() { let pre = 0, cur = 1; // iteratorオブジェクトは nextメソッドを持つオブジェクトを返す return { next() { // nextの中では返す値(value)と次で終わりかどうかを示すプロパティ(done)を返す [pre, cur] = [cur, pre + cur]; if (pre < 1000) return { done: false, value: pre }; return { done: true }; } } } } for (var n of fibonacci) { console.log(n); }
Generator Comprehension
Array Comprehensionの時にも出てきたけど、Generator関数もfor ofで値を加工して新しい配列を作ったりすることが可能。
var list = [1, 2, 3, 4]; // 実は (function*(){ for (var x of list) yield x })();と同じ var res = (for (x of list) x); var acc = ''; for (var x of res) { acc += x; } console.log(acc); // "1234"
Generators
Iterableなオブジェクトを作るときには最も効力を発揮するgenerators、さっきのfibonacciの例で使うとこうなります。
// 1000までの値を返すfibonacciを作る function* fibonacci(){ let pre = 0, cur = 1; while (pre < 1000) { // ここでdestructuringで値をswapさせる。 [pre, cur] = [cur, pre + cur]; // yieldで値を返す yield pre; } } for (let n of fibonacci()) { console.log(n); }
Symbol.Iteratorを使った例と比較するとgeneratorsがどれだけ使いやすいか分かるかと。
Modules
これまでモジュールの参照解決にはrequire.js使ったりBrowserify使ったりしてたんですが、ES6のロード機能によってこれらが言語仕様レベルで解決されます。
// Profile.js // exportで外から見えるようにすることが可能 export var firstName = 'David'; export var lastName = 'Belle'; export var year = 1973;
//ProfileView.js import {firstName, lastName, year} from './Profile'; function print() { console.log(firstName + ' ' + lastName + ' ' + year); //David Belle 1973 } print();
HTMLで読み込みたい場合はmodule属性にして読み込みます。
<script type="module" src="ProfileView.js"></script>
Numeric Literals
10進数や16進数の数字だけじゃなく、2進数とか8進数とかの数字を直接書けるようになります。
var binary = [ 0b0, 0b1, 0b11 ]; console.log(binary); //[0, 1, 3] var octal = [ 0o0, 0o1, 0o10, 0o77 ]; console.log(octal); // [0, 1, 8, 63]
Property Method Assignment
プロパティメソッドとして簡略的にメソッドが書けるように成りました。
var object = { value: 42, //直接toString()って書ける // 今までだと toString: function() {} って書く必要がある toString() { return this.value; } }; console.log(object); //{ value: 42, toString: [Function] }
Object Initializer Shorthand
オブジェクトの初期化を短く書けるようになります。
function getPoint() { var x = 1; var y = 10; // ここでオブジェクトにしている。 // 今までだと {x: x, y: y} って書く必要がある return {x, y}; } // {x: 1, y: 10} console.log(getPoint());
Rest Parameters & Spread
可変長パラメータによって以下のように関数を表現すること(Rest Parameters)も、配列を可変長パラメータに展開して渡す事(Spread)も可能です。
// ...itemsで可変長パラメータ function push(array, ...items) { // それをarrayにpush // 今までだと items.forEach(function(item) {array.push(item);});って書く必要がある array.push(...items); } var list = [1, 2, 3]; //list変数に数字をpush push(list, 4, 5, 6); console.log(list); //[1, 2, 3, 4, 5, 6] function add(x, y) { return x + y; } var numbers = [4, 38]; //配列を数字に展開している console.log(add(...numbers)); // 42 var a = [1]; var b = [2, 3, 4]; var c = [6, 7]; // こんな書き方も。配列を結合するのに便利 var d = [0, ...a, ...b, 5, ...c]; console.log(d); // [0, 1, 2, 3, 4, 5, 6, 7];
Template Literals
文字列内で変数展開、式展開することが可能です
var name = 'world'; //ここで変数展開する、これによってworldっていう文字列を埋め込んでいる var greeting = `hello ${name}`; console.log(greeting); // hello world var hoge = 1; var fuga = 2; // 式を評価することも可能。 var moga = `${hoge + fuga}`; console.log(moga); // 3 // 複数行の折り返しも書ける var multi = ` (-_-) | | | | `; /* (-_-) | | | | */ console.log(multi);
Promises
非同期処理の成功時と失敗時の切り分けができるようになる機能です。
function timeout(ms) { // Promiseのresolve関数を受け取る return new Promise((onFulfilled, onRejected) => { // 50%の確率でonFulfilled, onRejectedが呼ばれる setTimeout(() => Math.random() > 0.5 ? onFulfilled() : onRejected(), ms); }); } function log() { console.log('done'); } function error() { console.log('error'); } // onFulfilledが出たらdone、onRejectedだったらerrorと表示する timeout(100).then(log).catch(error)
Block Scoped Binding (Experimental)
let, constといったblockスコープです。JavaScriptの場合、スコープを表現するのにfunctionで囲む必要がありましたが、letを使うことで、functionだけではなくブレースやブラケットで囲まれた領域がスコープに成ります。
ただこれはExperimentalとされていて、通常はtraceurでコンパイルできません。experimentalフラグを立てる必要があります。
// block.js { var a = 10; let b = 20; const tmp = a; a = b; b = tmp; } // a = 20、aはvarで宣言しているのでブロックスコープの外からも参照可能。 console.log(a); // letで定義したbはブロックスコープの外からは解決できない、b is not defined console.log(b); // constもスコープの中でのみ有効、tmp is not defined console.log(tmp);
トランスパイルする際は以下のようにする
$ traceur --experimental block.js
ちなみにletとかconstに限らない話ですが、traceurにはfree-variable-checkerという未定義の変数をチェックする機能があります。これを使うとトランスパイル中に未定義の変数に対して警告を出すことが可能です。
$ traceur --experimental --free-variable-checker --out block_compiled.js --script block.js [Error: b is not defined]
Symbol (Experimental)
Symbolを使うことでJavaScriptにprivateな値を持たせることが可能です。
Symbolもexperimentalオプションが必要になります。
var object = {}; { // このブロックでしか有効じゃない値でSymbolを作る var visible = Symbol(); let invisible = Symbol(); // object自身は外から見えるが、symbolの値は同じsymbolでしか // 取れないためpropertyのkeyをblockスコープにしてからsymbolを使うと外から見えなくなる object[visible] = 42; object[invisible] = 84; // 42 console.log(object[visible]); // 84 scopeの中では有効 console.log(object[invisible]); } // 42 visibleな値は取れる console.log(object[visible]); // {} invisbleな値が見えてない console.log(object);
Traceur が提供する言語機能のまとめ
ここまでがES6からES5にトランスパイル可能な機能一覧です。
これ以外にもES6で提案されている仕様はあるんですが、ブラウザの機能に深く食い込んでてpolyfillで解決できないどうしようもない機能(WeakMapとか)はおそらくTraceurでは実装できないでしょう。またProxy APIはFireFoxでは実装が進んでいるんですが、現時点ではTraceurではトランスパイルできません。
traceurが提供するES6の機能と各ブラウザベンダが提供する機能の比較一覧は以下の表を見ると分かりやすいです。
ES6以外のTraceur Compilerが提供する機能
実はES6からさらに進んだ機能、ES7で提案されている機能もTraceur Compilerでは扱うことができます。
Async Functions (Experimental)
async/awaitと呼ばれるC#にある機能ですね。
async修飾子をfunctionにつけることでawait句という特別な構文が使えるようになります。
先ほどのPromiseの例をAsync Functionで書くとこうなります。
function timeout(ms) { // Promiseのresolve関数を受け取る return new Promise((onFulfilled, onRejected) => { // 50%の確率でonFulfilled, onRejectedが呼ばれる setTimeout(() => Math.random() > 0.5 ? onFulfilled() : onRejected(), ms); }); } function log() { console.log('done'); } function error() { console.log('error'); } // Promiseをwrapしてthenの部分とcatchの部分を分割することができる async function asyncLog() { await timeout(500); log(); } async function asyncLogger() { await asyncLog().catch(error); done(); } asyncLogger();
Promoseだけじゃなく、Generatorsも対象としているようで、言語仕様レベルで非同期処理を同期処理っぽく書くためのフローを提供しようとする枠組みですね。Node.jsにはcoというライブラリがありますが、それと非常によく似ています。
まだES7のproposalというステータスなので、今後が楽しみではあります。もちろんexperimentalです。
Types (experimental)
型の情報を関数の引数に明示できるという機能です。
JavaScriptは動的型付け言語なので、型の情報を明示出来るとそれだけでも割りと夢が広がります。
といってもトランスパイル時に警告するとかそういう機能はtraceurは提供していません。
Typesを書くことで実行時に関数内で引数チェックしてくれる仕組みが提供されています。
これにより、関数呼び出しの際の引数の型チェックを実装しなくても言語側で実装してくれるという嬉しい機能ですね。ただ重ねて言いますが、ランタイム時のチェックしかしてくれません。
もしもTypesが標準化されたらESLintとかそういうLintツール上でチェックしてくれると嬉しいですね。
class TypeTest { // 型情報として 第一引数がstring, 第二引数が numberであることを書く。 constructor(x : string, y : number) { this.x = x; this.y = y; } } // コンストラクタを呼び出した時に敢えてstringとnumberの順序を逆にする。 // この時にランタイムエラーで実行に失敗し、警告が出る。 new TypeTest(1, "hoge");
このコードを実行するには多少前準備が必要です。全然手順が書かれてないのでドキュメントにない機能なんですが、使えることには使えます。
まず、assert関数を使えるようにするためにここからassertのファイルをcloneしてください。この中にあるsrc/assert.jsを使います。その上でtype-assertionsオプションとtype-assertion-moduleオプションを使いましょう。
# type-assertionsとtype-assertion-moduleオプションを使う、moduleの方にはassertライブラリへのpathが必要。 $ traceur --type-assertions --type-assertion-module=assert/src/assert --experimental --out types_compiled.js --script types.js
この後、types_compiled.jsを実行すると、以下の様なエラーが出ます。
Error: ModuleEvaluationError: 'Error: Invalid arguments given! - 1st argument has to be an instance of string, got 1 - 2nd argument has to be an instance of number, got "hoge"'
これにより、引数がアンマッチしていることが警告から分かるようになります。
Annotations
メタ情報をアノテーションとして埋め込むことができます。
// this.nameだけつける var SimpleAnnotation = function(){ this.name = 'Simple'; }; var NestedAnnotation = { // this.name と 引数を持つ Args: function(arg){ this.name = 'Simple'; this.arg = arg; } }; // クラスにAnnotationを付ける @SimpleAnnotation class Foo { constructor() { // 値はannotationsプロパティから得る // [{ name: 'Simple' }] console.log(Foo.annotations); } // 引数 barを入れる @NestedAnnotation.Args("bar") bar() { } } var foo = new Foo(); foo.bar(); // bar をprototypeから得る // [ { name: 'Simple', arg: 'bar' } ] console.log(Foo.prototype.bar.annotations);
実行すると@から始めたAnnotationをインスタンスの中とインスタンスの外側からも値を見ることができるようになります。
実はTypesやAnnotaionsは現在Angular v2.0の開発時に使っている機能のようです。型情報やアノテーションが付けられた方が開発効率が良いらしく、次のES6にあわよくば入れたいが、無理ならES7、みたいな位置づけで提案しようとしているんじゃないかと推測してます。
Annotationを付けることでAngular v2.0にどういう機能が加わるのか少し楽しみではあります。
第二章まとめ
- ES6で提供される機能一覧
- ES7で提供される機能一覧
第三章 traceur compilerの使いどころと類似ツール
traceur compilerのイケてないところ
ここまで学んできて、個人的にtraceurイケてないと思うところは3つほどあります。
- traceur専用のランタイムスクリプトが必要
- 実行速度が遅くなる可能性がある
- 出力されるコードが読みづらい
traceur 専用のランタイムスクリプトが必要
正直一番最初に学んだ時におもったのがこれで、「ES5にトランスパイルしてくれると言ってもそのまま実行できるコードが出力されるわけじゃないのか。。。」と思いました。runtimeはそこまで複雑なことをしている訳じゃないんですが、このruntimeがないと動きません。
destructureとかblocking scopeを使うとどうしてもトランスパイル後の実行時にES5では検証されない値にアサーションを追加する必要があります。
この辺りは実装の仕方だと思いますが、そういうアサーションを提供する際に直接ES5にトランスパイルした時のコードに入れるか、別アサーションライブラリとして切り出して利用させるかといった問題が発生します。
traceurでは後者を選択し、ES5トランスパイル後のスクリプトに複雑なことを入れない反面、traceur-runtime.jsを読み込ませることにしています。
アサーション以外にも標準機能の拡張系の処理だったり、generators、PromiseだったりがES6には加わっているため、その手のpolyfillもtraceur-runtimeには加わっています。
実行速度が遅くなる可能性がある
特にletを使った時に顕著なんですが、Traceurのトランスパイル後の速度を測ってみるとやっぱりそこまで速くありません。
この辺りは若干しょうがない気もしていて、traceurのメインゴールはES6を今のブラウザで使えるようにすることなので、
実行速度の改善は今のところ二の次です。とはいえ、実行速度が明らかに落ちるletとかは辛いです。
traceur-compiler側では今のところletはexperimentalなので、正式な機能として提供してません。
またblock scopeを使った時のパフォーマンスissueとかも報告されており、作者のコメントによればもう少し速くする方法はあるようですが、今のところ時間がないので未対応という感じですかね。
出力されるコードが読みづらい
これも割りとしょうがないんですけど、generatorとかlet使うと出力後のコードがかなり複雑になります。letが遅く、experimentalである理由ですね。ここでletを使ったコードの出力結果を見てみましょう。generatorの時に登場したfibonacci関数に再登場してもらい、出力結果を見てみましょう。
// BEFORE // 1000までの値を返すfibonacciを作る function* fibonacci(){ let pre = 0, cur = 1; while (pre < 1000) { [pre, cur] = [cur, pre + cur]; // yieldで値を返す yield pre; } } for (let n of fibonacci()) { console.log(n); }
// AFTER System.register("a", [], function() { "use strict"; var $__3 = $traceurRuntime.initGeneratorFunction(fibonacci); var __moduleName = "a"; function fibonacci() { var $__2, pre, cur; return $traceurRuntime.createGeneratorInstance(function($ctx) { while (true) // whileで無限ループさせて各種状態によって次のステップに進むか値を返すか例外を投げるか決めてる // ちなみにJSXもgenerator/yieldでは似たようなことしてる。 switch ($ctx.state) { case 0: pre = 0, cur = 1; $ctx.state = 9; break; case 9: $ctx.state = (pre < 1000) ? 5 : -2; break; case 5: ($__2 = [cur, pre + cur], pre = $__2[0], cur = $__2[1], $__2); $ctx.state = 6; break; case 6: $ctx.state = 2; return pre; case 2: $ctx.maybeThrow(); $ctx.state = 9; break; default: return $ctx.end(); } }, $__3, this); } // ここがforループでletを使った時に問題になりそうなコード for (var $__0 = fibonacci()[$traceurRuntime.toProperty(Symbol.iterator)](), $__1; !($__1 = $__0.next()).done; ) { try { // まずここで例外を発生させる! throw undefined; } catch (n) { // catch節の中で例外用の変数nに値を代入して使っている。 // 多分ここがループで使うとものすごく遅い。 // このcatch節の外でletで宣言したnは見えない、なぜならn変数はこのcatch節の中でしか有効じゃないから。 { n = $__1.value; { console.log(n); } } } } return {}; }); System.get("a" + '');
とまぁこんな感じでものすごく見難いコードになります。かろうじて中でやってることは分かりますが、letをforループ内で使うと毎回例外を投げるという酷いコードになってしまうので、今のところexperimentalというのも頷けます。
というわけで、
runtimeスクリプトが必要なのは実装方針なのである程度しょうがないとして、traceurでexperimentalとされている機能を使うのはあまり得策ではありません。
機能的にも性能的にも安定しなくなります、もしもexperimentalな機能を使うなら、完全に学習用だと割りきって使う方がよいでしょう。
その他の機能もシビアに性能を求める場所では使わないほうが良いでしょう。generatorsの生成コードを見てもらって分かるかと思いますが、かなり富豪的に実現しようとしているので、それよりは愚直に書いた方が性能は上でしょうし、それは他の機能においても同様のことが言えます。
「現時点では」まだ仕様も固定されていないES6に手を出そうとしている時点で人柱になる覚悟はないといけないし、明日には仕様が変更される可能性があることを考慮しつつ使うことをオススメします。
それでもES6の仕様が固定された上で生成されるコードの性能面が安定するなら学習用じゃなく、一般的に利用してもよいと思っています。長期的に見ればES6 + traceurで書いて、今のうちにES6への学習コストを払っておく事に利点はあると感じています。
類似するツール
実はtraceur-compilerだけじゃなくて、後発で同様のことを実現するツールはいくつかあります。今のところ機能面ではtraceurが一番多いですが、human readableなコードを生成する点やruntimeエンジンが不要な点でtraceurよりも優れていると思えるものもあります。
- es6-transpiler traceur-compilerが専用のruntimeエンジンが必要なのに対してruntimeスクリプト不要のjavascriptを出してくれる。また、outputされるコードの中身も読みやすい。letの時にはtry-catchを使わないことが明記されており、transpile時に警告を出してくれる。ただし、symbol, generators, moduleといった機能は未実装。
第三章 まとめ
- traceurのイケてない所
- traceurの使いドコロ
- 類似するツール
まとめ
- traceur-compilerの概要を説明
- traceur-compilerがサポートしている言語仕様を説明
- traceur-compilerの使いドコロを説明
- 類似するツールについて説明
ES6の機能群を見ているとJavaScriptが割りとモダンで新しい言語になろうとしている反面、各ブラウザベンダの機能サポートに差があるので、クロスブラウザでサポートする場合、こういうtranspilerが今後必要になってくるのではないかと感じています。
これを見て少しでもES6の機能に興味が出てくれると幸いです。
参考資料
*1:runstant++
*2:どうでもいいことだけど、この人がid:teppeisさんにものすごくアイコンが似ててビックリする