ここしばらく仕事で忙しく、久しぶりの更新となってしまいましたが、ここ1週間程時間に余裕が出来きましたので、改めて更新していこうと思います。
今回のネタはAltJSに関してです。
JSX、CoffeeScript、Dart、その他様々な言語がAltJSとして存在していますが、個人的に気になっているHaxeとTypeScriptで同一のHTMLで同じ構成をもったコンテンツを作って比較してみました。
まずはHaxe、TypeScriptそれぞれの特徴です。
Haxe
- http://haxe.org/
- 2005年~(現時点での最新バージョンは3.0)
- 静的型付け(ただし動的型も使用可能)のオブジェクト指向言語
- ActionScript3.0に非常に類似
- JavaScriptだけでなく、Flash/AIR/C++/PHP/Java/C#/Neko他に変換
TypeScript
- http://www.typescriptlang.org/
- 2012年~(現時点での最新バージョンは0.9)
- MicroSoftが開発するオープンソースの言語
- JavaScriptに型やクラス、名前空間を追加
- JavaScript との親和性が高く、可読性の高いJavaScriptが出力される
この二つに注目している理由は、まず自分にとっての学習コストが低いことと、今後の将来性です。
学習コストに関して、HaxeはActionScript3.0に酷似していてFlasherにとっては非常に親しみやすい言語であるという事と、慣れ親しんだFlashDevelopでの開発が出来る事です。
TypeScriptは基本的な文法は既存のJavaScriptと同じでJavaScriptに型やクラス、名前空間を拡張した言語であるので、JavaScriptの学習の延長で習得可能ということです。
今後の将来性でみると、Haxeは数あるAltJSの中では2005年~という非常に古い歴史を持っていて順調にバージョンアップを重ねているという信頼感があります。また言語がActionScript3.0に酷似していて、フロントエンドエンジニア(特にFlasher)が移行しやすい環境があるのではないかと思います。勝手な事ですが個人的にはCreateJSと結合してFlashに組み込まれれば最高だと思います。
次にTypeScriptの将来性で言うと、まずMicroSoftが開発、設計しているという点が大きいかと思います。それと既存のJavaScriptとの親和性が非常に良く、導入にあたりデメリットが少ないため、今後より実案件に採用される事が多くなるのではないかと思っています。個人的には現時点でのバージョンが0.9で次期バージョンの1.0での大幅な仕様変更があるかもしれませんので、実案件への採用はvar1.0登場まで待ちたいところですが。
では実際に作って比較してみたサンプルの説明をします。
- TypeScript版 (ソースコード)
- Haxe版 (ソースコード)
今回、注目しておきたいポイントは以下の3つです。
- パッケージ管理(入れ子、コードの分割)
- クラスの利用(パブリック、プライベート、継承、インターフェイス)
- JavaScriptのライブラリと既存資産の利用
これらに支障が出なければ、大小に限らず実案件での使用が現実的になってくるのではないかと思います。それではHaxe、TypeScript共に作成したコードを比較します。
Haxeサンプル
このサンプルで作成したパッケージは、以下の構成になります。
display : ブラウザ上に表示される要素
- navigation : ナビゲーション
- scenes : シーン
logic : 定数、ユーティリティ等
lib : 再利用目的で作成する共有ライブラリ
- button : ボタン機能
- display : 表示機能
Haxeではパッケージと同様の入れ子構造をもったディレクトリを作り、1hxファイルで1クラスを定義するという管理方法が実現できます。これを利用してコードを分割することで、コードの生産性と保守性が保たれます。またコンパイル時にimportしたファイルを判別して自動的に結合、出力するので、無駄のないJavaScriptコードが出力出来ます。
はじめに実行されるコードはMain.hxのMainクラス(設定ファイルにて変更可能)の静的関数main()になります。静的関数が初期実行のトリガーとなりますので、Mainクラス内では必然的に静的メンバを使用していくことになります。(※静的メンバ内では静的メンバにしかアクセスは出来きないため。)
Main.hx
package ; import display.navigation.Navigation; import display.scenes.Scenes; import js.Browser; import js.JQuery; import logic.Const; class Main { static inline function _(str:String):JQuery {return untyped $(str);} static private var _navigation:Navigation; static private var _scenes:Scenes; static public function main():Void { Browser.window.onload = Main._init; } static private function _init(e:Dynamic):Void { Main._navigation = new Navigation(_("#nav")); Main._scenes = new Scenes(_("#scenes")); Main.changeSceneEvent(Const.ID_SCENE_01); } static public function changeSceneEvent(id:String):Void { Main._navigation.changeScene(id); Main._scenes.changeScene(id); } }
jQueryを利用するにあたってはjQueryExternという、jQueryラッパーライブラリがあります。コンパイルを通すためのjQuery型指定のみが記述されたライブラリです。ただ問題はjQuery以外の別のライブラリを使用したい場合に、対応するラッパーライブラリを探すことが出来るかというところです。ダウンロードしたjQueryExternを見る限り、自作するのは厳しいかもしれません。最終的な手段としてはDynamicという動的型付けを行えばコンパイルを通すことが出来ますが、せっかく静的型付け言語を選択しているので避けたいところです。
またHaxeでは$を変数名に付けることが出来ませんので、常にjQuery()と記述しなければなりません。「static inline function _(str:String):JQuery {return untyped $(str);}」といったヘルパ関数を用意することで_()と省略して記述することが可能になります。
HaxeはJavaScriptではありませんので、DOMのAPIを叩きたい時には、「Browser.window.onload = Main._init;」のように、HaxeAPIを介して叩く必要があります。このため既存JavaScriptコードの移植に関しては注意が必要になります。JavaScriptでやっていたことをHaxeでやるにはどうしたらいいんだ?って考える必要が毎回発生することは非常にデメリットです。正直ドキュメントも揃っていない感はあります。
display.scenes.Scenes.hx
package display.scenes; import js.Browser; import js.JQuery; import display.scenes.Scene; import lib.display.IDisplay; import logic.Collection; class Scenes { static inline function _(str:String):JQuery {return untyped $(str);} private var _target:JQuery; private var _scene01:Scene; private var _scene02:Scene; private var _scene03:Scene; private var _collections:Array; public function new(target:JQuery) { this._collections = new Array(); this._target = target; // var scene01:JQuery = this._target.find("#scene01"); var scene01Id:String = scene01.attr("data-id"); this._scene01 = new Scene(scene01Id, scene01); this._collections.push(new Collection(scene01Id, this._scene01)); // var scene02:JQuery = this._target.find("#scene02"); var scene02Id:String = scene02.attr("data-id"); this._scene02 = new Scene(scene02Id, scene02); this._collections.push(new Collection(scene02Id, this._scene02)); // var scene03:JQuery = this._target.find("#scene03"); var scene03Id:String = scene03.attr("data-id"); this._scene03 = new Scene(scene03Id, scene03); this._collections.push(new Collection(scene03Id, this._scene03)); } public function changeScene(id:String) :Void { for (i in this._collections) { var target:IDisplay = i.getTarget(); if (i.getId() == id) { target.open(); } else { target.close(); } } } }
コンストラクタ関数としてnew()が用意されています。
Haxeでは「this」はインスタンスを指しますので、JavaScriptのように厄介な存在ではありません。心補く「this」を使ってください。
修飾子には「public」「private」のみならず「static」「dynamic」「override」が用意されています。省略すると「private」となるところが個人的には好感が持てます。ちなみにActionScript3.0ではデフォルト「public」です。
その他のポイントとしては、forループで「for(var i = 0; i < length; i ++){}」のような構文は使えません。必ず「for( i in 0...a.length ) {}」を使用してください。
display.scenes.Scene.hx
package display.scenes; import js.Browser; import js.JQuery; import lib.display.BaseDisplay; import lib.display.IDisplay; class Scene extends BaseDisplay implements IDisplay { static inline function _(str:String):JQuery {return untyped $(str);} public function new(id:String, target:JQuery) { super(id, target); } }
lib.display.IDisplay
package lib.display; interface IDisplay { function getId() :String; function open() :Void; function close() :Void; }
クラスを宣言するとき、一つのクラスに対してextends、複数のクラスまたはインターフェースに対してimplementsを指定することができます。クラスを継承するとき、そのクラスはpublicまたはprivateな全てのstaticでないフィールドを引き継ぎます。staticな変数とメソッドは継承しません。
また、親クラスと同じ数と型の引数を持ったメソッドをオーバーライドして再定義することができます。メソッドをオーバーライドしたときは、親クラスのメソッドへのアクセスに「super」を使用してください。
TypeScriptサンプル
TypeScriptではモジュールとして名前空間を定義することができます。さらにモジュールを入れ子にすることによって、パッケージのようなものを作成することが出来ます。
ただActionScript3.0のようなパッケージ管理を期待してはいけません。クラス毎にファイルを分割することはもちろん可能ですが、importしたクラスを自動的に結合して一つのJavaScriptファイルを作成してくれるといったことはありません。分割したファイルはHTMLで読み込まなくてはなりません。コンパイル時のオプションで、複数のTSファイルを指定して、結合したJavaScriptを作成することは可能ですが、ファイルを作成するごとにTSファイルの指定をし直すことは、あまり現実的ではないように思えます。
また、同一ファイル内に複数のモジュールとクラスを記述したとしても、そのクラスを利用する際には、予め定義(使用する行より上部に記述)していないとエラーとなってしまいます。これらの事を考えるとTypeScriptでのモジュールとはあくまで、他のライブラリ、もしくは内部クラスでの競合を避けるために使用する程度と考えた方が無難だと認識しました。
ActionScript3.0のようなimportのような強力なものではありませんが、ファイルを分割することは可能です。–outオプションで出力ファイル名を指定すると、///
TSファイル内でHaxe版のパッケージ構造を意識してモジュールを入れ子にしています。TSファイル自体は今後の再利用を最低限考えて、index.tsとlib.tsファイルに分けています。
index.ts : メインとなるTypeScriptソースファイル
jquery.d.ts : jQueryラッパーライブラリ
lib.ts : 再利用目的で作成する共有ライブラリ
HaxeのMainクラスのように、初期実行のトリガーとなるものはありません。以下のように自前でMainクラスからインスタンスを作成して、コンストラクタ関数を叩く必要があります。
index.ts
$(function () { var main:display.Main = new display.Main($("body")); main.init(); });
index.ts
module display { export class Main { private _$target:JQuery; private _navigation:navigation.Navigation; private _scenes:scenes.Scenes; constructor(target:JQuery) { this._$target = target; this._navigation = new navigation.Navigation(this, $("#nav")); this._scenes = new scenes.Scenes(this, $("#scenes")); } public init() :void { this._navigation.init(); this._scenes.init(); this.changeSceneEvent(logic.Const.ID_SCENE_01); } public changeSceneEvent(id:String = logic.Const.ID_SCENE_01) :void { this._navigation.changeScene(id); this._scenes.changeScene(id); } } }
jQueryを利用するにあたってはjQuery型定義ファイルをダウンロードして作業フォルダに入れ、「///
TypeScriptには直接JavaScriptを記述することが可能ですので、HaxeのようにDOMのAPIを叩きたい時にHaxeAPIを介して叩く必要がありません。このため既存JavaScriptコードの移植に関してそのまま利用できます。これは他のAltJSには無いTypeScriptの一番魅力的な部分だと思います。
index.ts
module display { module scenes { export class Scenes { private _root:display.Main; private _$target:JQuery; private _collections:Array; private _scene01:Scene; private _scene02:Scene; private _scene03:Scene; constructor(root:display.Main, target:JQuery) { this._root = root; this._$target = target; this._collections = []; // var scene01:JQuery = this._$target.find("#scene01"); var scene01Id:string = scene01.attr("data-id"); this._scene01 = new Scene(this._root, scene01Id, scene01); this._collections.push(new logic.Collection(scene01Id, this._scene01)); // var scene02:JQuery = this._$target.find("#scene02"); var scene02Id:string = scene02.attr("data-id"); this._scene02 = new Scene(this._root, scene02Id, scene02); this._collections.push(new logic.Collection(scene02Id, this._scene02)); // var scene03:JQuery = this._$target.find("#scene03"); var scene03Id:string = scene03.attr("data-id"); this._scene03 = new Scene(this._root, scene03Id, scene03); this._collections.push(new logic.Collection(scene03Id, this._scene03)); } public init() :void { var i = 0, max; for (i = 0, max = this._collections.length; i < max; i = i + 1) { var info:logic.Collection = this._collections[i]; var target:lib.lib_display.IDisplay = info.getTarget(); target.init(); } } public changeScene(id) :void { var i = 0, max; for (i = 0, max = this._collections.length; i < max; i = i + 1) { var info:logic.Collection = this._collections[i]; var target:lib.lib_display.IDisplay = info.getTarget(); if (info.getId() == id) { target.open(); } else { target.close(); } } } } } }
コンストラクタ関数としてconstructor()が用意されています。
メンバ変数は「var」キーワードは付けずにあとは通常の変数と同じように記述します。メンバ関数は「function」のキーワードは書かずに関数名、引数、本体と書いていきます。
修飾子には「public」「private」「static」が用意されています。省略すると「public」になります。
TypeScript の「this」はそれが書かれている場所によって意味が異なります。コンストラクタ、メンバ関数、メンバアクセサにおいては、「this」はそのインスタンスを指し、関数定義や通常のfunction式、グローバルモジュールでは「this」は呼び出した文脈によって変わります。しかしクラスの内部でクラスメンバを指定する際には「this」を省略することはできません。ここでjQueryを使用していると問題が発生します。
constructor() { this.target.on("click", onClick); } public onClick(e:JQueryEventObject) :void { if (this.isActive) { console.log("_onClick : " + this.id); } }
clickイベントで実行された関数onClickの「this」は「target」を指してインスタンスを指さないので、this.isActiveはundefinedになってしまいます。これは以下のようにプライベート変数に「this」を予め代入しておく事で解決できます。
constructor() { var scope = this; this.target.on("click", function(e:JQueryEventObject){ scope.onClick(e); }); } public onClick(e:JQueryEventObject) :void { e.preventDefault(); if (this.isActive) { console.log("_onClick : " + this.id); } }
また別の解決方法としてアロー関数式が用意されています。上記のような場合はアロー関数式を使うと、「this」がちゃんとインスタンスを指すようになります。これは、コンパイルするときに自動的にインスタンスを参照する変数「_this」を定義、保存しておいてから参照するようにしてくれるからです。
this.$target.on("click", (e:JQueryEventObject)=> { this.onClick(e); }); public onClick(e:JQueryEventObject) :void { e.preventDefault(); if (this.isActive) { console.log("_onClick : " + this.id); } }
index.ts
module display { module scenes { export class Scene extends lib.lib_display.BaseDisplay implements lib.lib_display.IDisplay { private _root:display.Main; constructor(root:display.Main, id:string, target:JQuery) { super(id, target); this._root = root; } public init() :void { } } } }
lib.ts
module lib { export module lib_display { export interface IDisplay { init() :void; getId() :string; open() :void; close() :void; } } }
クラスを宣言するとき、一つのクラスに対してextends、複数のクラスまたはインターフェースに対してimplementsを指定することができます。
また、親クラスと同じ数と型の引数を持ったメソッドをオーバーライドして再定義することができますが、特に修飾子による記述はないために、知らない間に重要なメソッドを上書いてしまいかねません。「private」なクラスメンバのオーバーライドはコンパイルエラーとなってしまいますので、親クラスのメンバに関しては基本「public」属性とならざるを得ません。メソッドをオーバーライドしたときは、親クラスのメソッドへのアクセスに「super」を使用してください。
まとめ
あらためて「パッケージ管理」「クラスの利用」「jQuery等ライブラリの利用」に関しての個人的評価をすると、
Haxe | |
---|---|
パッケージ管理 | コードが分割できクラス単位での管理がしやすく、コンパイル時にimportしたファイルを判別して自動的に結合、出力するので、無駄のないJavaScriptコードが出力出来ます。 |
クラスの利用 | ActionScript3.0とほぼ同様の機能を有しています。 |
JavaScriptのライブラリと既存資産の利用 | ライブラリ使用に関してはラッパーライブラリが存在していれば問題ありませんが、なかった場合は動的型付けで対応する事になりそうです。 JavaScriptを記述したい場合にDOMのAPIを直接叩く事が出来ないために、HaxeのAPIに依存してしまいます。 |
TypeScript | |
---|---|
パッケージ管理 | モジュールとして名前空間を定義することができますがファイルが分割しづらく、またクラスを利用する際には、予め定義(使用する行より上部に記述)していないといけないといった事を考えると、とてもパッケージ管理とは言い難い状況です。 var1.0でその点が改善されればいいのですが、今のところ不明です。 |
クラスの利用 | クラス内部での「this」の扱いやオーバーライドに関しては多少不満がありますが、工夫次第で解決出来る問題の範囲かと思います。 |
JavaScriptのライブラリと既存資産の利用 | 直接JavaScriptを記述することが可能で、既存JavaScriptコードをそのまま移植、利用することができます。 |
といった感じでしょうか。
今回はあくまでサンプルなので、クラスベースにこだわって記述したところで何のメリットもないソースになっている感じはありますが、実際の案件においてはOOPの実装が容易なクラスベースのオブジェクト指向言語にこだわることは、生産性と保守性の点でとても意味があることだと思っています。
Haxeはその点文句の言いようもないのですが、JavaScriptライブラリを利用したり、以前のJavaScriptコードを利用する分には慎重にならざるを得ないという感じです。
TypeScriptはあくまでJavaScriptを拡張したイメージなので、その点ではいまいちな点が多いですが、JavaScriptとの親和性を考えると、現実的な選択肢だったりします。
まあ、大抵の場合はJavaScriptパターン等に乗っているベストプラクティスを意識して書いていけば、必要最低限のコードで美しく記述することが出来ると思いますが。