正宗です。JavaScriptの開発のためにと思ってすごく今さらなんですがCoffeeScriptを導入したので、そこらへんをちょっとまとめておきたいと思います。
そうはいっても、既にO’reillyからCoffeeScriptについて本が出ています。さらにそれは作者によってオープンソースで公開されていて、さらにさらにminghaiさんがこちらに和訳したものを公開されております。
この本を読めば、特に他に何か読む必要あるの?というくらい、CoffeeScriptについては既にまとまった記事がネットでも書籍でも存在しているので、わりかし今回自分はこの記事を書くに当たって今まで以上に消極的だったりします。
みなさんは私が今書いているこの記事のことなんか忘れて、すみやかに上記リンク先に飛んでください。
いちおう自分は、「あれ、ActionScript(以下AS3)でこう書いてたやつ、CoffeeScriptではどう書くんだっけ?」みたいな場面にさしかかったとき、本を隅々まで調べなくてもすむように、CoffeeScriptに慣れるまでのあいだ自分の備忘録的にここにまとめておくことにします。
できるだけAS3とCoffeeScriptとJavaScriptのコードをのせていますが、AS3に関しては説明を簡易にするためpackageや型指定を意図して省けるところは省いて書いています。
お品書きはこちら。
なんでAS3っぽく書く必要があるの?
いきなりですが私の退屈な御託から始まりますので、ぜひこの項目は読み飛ばしてください。
私は以前にも何回かFlashの開発手法と似たようなことをJavaScriptでもできないかというような取り組みを記事にしたことがあり、
そのせいもあって、「どうにかしてでもFlashとJavaScriptは一緒のような開発方法をしなくてはいけない!」みたいな意見の持ち主に思われていることが多いのですが、実際にはそんなことありません。
どうしてもブログの記事だけでは言葉足らずな部分もあるので、そもそも自分が業務で行っている範囲内での技術的な覚え書きを溜めていくためのブログなので極力自分の考えなんかは挟まないようにしていることもあって、もしかしたら極端な印象だけが一人歩きするのかもしれません。
実際には、AS3であれJavaScriptであれCoffeeScriptであれその言語にみあった方法で進めていくべきだと思っています。
特に、この言語にはこの機能がないからあの言語に劣る、といったようなランク付けは全く意味がないので、ケースに応じた割り切った方法が必要だと考えています。
じゃあなんでこんな記事を書いているの?考えと行動が矛盾していないか、と思われるかもしれませんが自分の中では矛盾していません。
ランク付けは意味がないですが把握することによってより使いこなせるようになると考えているからです。
違う言語どうしの機能を比べてランクをつけることには全く意味がないですが、言語間の違いを把握しておくほうが今まで培ってきたデザインパターンやアルゴリズムやライブラリなどの再利用性や移植性がより高まると思います。
そう書くと今度はコードの再利用や移植性こそが至上主義であると主張しているように思われるかもしれませんがそれもすこし違います。
私はどちらかというとコードの可読性至上主義なので(まだそのために学ぶ門を叩いたどころか前に立ってるだけの段階なので、いま私のコードの可読性が高いわけではありませんが…)、他の言語の人が読んでも何やっているかわかるように書くことが可読性が高まる場合が多いと思っているので、そのために言語間での似たパターンを探っているというところです。
少し具体例を出してみます。
var Friends, Person,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Person = (function() {
function Person(name) {
this.name = name;
this.getName = __bind(this.getName, this);
}
Person.prototype.getName = function() {
return this.name;
};
return Person;
})();
Friends = (function(_super) {
__extends(Friends, _super);
function Friends(name) {
Friends.__super__.constructor.call(this, name);
}
return Friends;
})(Person);
JavaScriptでのクラス開発に慣れていないと、関数リテラルが飛び交ってて上のコードが何を何しているソースなのか、ぱっと見ただけではわからないかもしれません。
これでもJavaScriptでのクラス開発に慣れた方には、少なくともPersonクラスを継承してFriendsクラスを作っているんだなというくらいは、わかるように工夫したつもりではあります。
次に同じものをAS3で書くとこうなります。
public class Person
{
private var _name:String;
public function Person(name:String){
_name = name;
}
public function get name():String{
return _name;
}
}
public class Friends extends Person {
public function Friends(name){
super(name);
}
}
こちらのほうは他の言語を使われている方も何をしているのかぱっと見ただけでわかりやすいかもしれません。
重要なのは、どちらがわかりやすいから優れているということではなくて、できるだけ何をしているのかわかりやすく書くためには言語間の違いを理解しておくと便利だと思っているということです。
あと、それならワンソースマルチユースが最適じゃないという声もありますが、私はわりとワンソースマルチユースに対してあまり過度な期待はしていない派です。
むしろ逆で、それぞれに合ったソースを用いるからこそ移植性を高めた書きかたを知っておくことは無駄ではないと思っています。
もちろんワンソースマルチユース断固拒否というわけでもないです。実際AIRアプリの開発なんかはそれこそワンソースマルチユースの最たる例ですし業務でも場合に応じて使い分けています。
長々となんかとりとめのないことを書きましたが、方法を書いたつもりがいつのまにか極論だけ一人歩きしてしまうといったことはよくあることなので、この機会に少しでも自分の考えを前提条件としてわかってもらえたらと思って書きました。
我ながら毎度毎度、能書きが長すぎると思いますが自分のブログなのでまあこんなもんでいいですよね。
で、なんでCoffeeScriptなの?
なぜ今CoffeeScriptなのか?そもそもメタ言語でAS3的な開発を行いたいのであれば何もCoffeeScriptじゃなくてHaxeやTypeScriptなど、もっと親和性の高い言語があります。
メタ言語に限らず開発環境そのものとしてAdobe Edge、JavaScriptライブラリでもCriateJSなどなど、FlashやAS3のノウハウをそのままJavaScript開発に持って行くことに対して今や環境は贅沢すぎるほど整っているといえます。
そもそもCoffeeScriptではなくて素のJavaScriptを使ったほうが、AS1のときはお互い全く同じ言語だったこともあってよっぽどAS3と親和性が高いです。
なのになぜCoffeeScriptなのか?
ひとつめの理由としては、私がまだCoffeeScriptしか触っていないからというのがあります。
もうひとつは、順番が逆なのです。AS3っぽい開発手法を取り入れたいからCoffeeScriptを導入したのではなくて、CoffeeScriptを導入したからこれでAS3っぽい手法は使えるか試してみたということなのです。
AS3とCoffeeScriptの機能比較
ということで早速ですがまずは機能比較したいと思います。それぞれの言語の全部を網羅することは難しいので、AS3的に開発したいといえばやはりクラスでの開発がメインになってくるので、そこらへんをまとめてみました。
| – | ActionScript3 | CoffeeScript |
|---|---|---|
| クラス | ok | ok |
| publicなクラスメンバ | ok | ok |
| public staticなクラスメンバ | ok | ok |
| 継承 | ok | ok |
| デフォルト引数 | ok | ok |
| 可変長引数 | ok | ok | privateなクラスメンバ | ok | ※後述 |
| private staticなクラスメンバ | ok | ok |
| getter setter | ok | whoops |
| interfaceの定義と実装 | ok | whoops |
| 名前空間 | ok | ※後述 |
| 定数 | ok | whoops |
かなり荒いですがいかがでしょうか?AS3的なノリでおおざっぱに言うとCoffeeScriptというかJavaScriptはdynamicなクラスでpublicなクラスメンバ持ってる、わりと胃がキュッとなる仕様だと思います。
なんどもいうようにもちろん、何と比べて何々が無いからどうということではなく、いったんこうやって洗い出してしまえば出来ないものは出来ないと決めて割り切って開発を進めることが出来そうです。自分としては「これはどうやったら出来るようになるんだろう?何か出来る方法はないか?」と試行錯誤する時間が減らせるのでだいぶ気が楽になりました。
クラスメンバをprivateに閉じ込めておくことが出来ないと言うことは、当然protectedやfinalも使えないということでさすがに最初びびりましたが今後自分なりにコーディングルールを作って対応したいと思います。
Interfaceが使えないのは、これもいっときはものすごく不安になりました。なぜなら私が今ちょうど学習してるオブジェクト指向のデザインパターンでは「場合によって使い分けることがもちろん大事なんだけど、継承よりもコンポジションやインターフェースを使うほうがより柔軟で堅牢なポリモーフィズムが作れる場合が多いぞよ」みたいなことが書かれてあって、自分がまだそのことにピンときていないだけにInterfaceをとにかく慣れようとしていた時期だったからです。今後の学習の面で少し心配が残りますが、しかたないのでまあこのへんも一旦は割り切ることにします。
※後述privateなクラスメンバに関しては、コンストラクタ内で定義することで似たようなことは出来そうではありました。しかしながらインスタンスを生成するたびにインスタンス毎にメソッドを作ってしまうので導入を諦めました。
名前空間に関しても、JavaScriptではいにしえよりObjectオブジェクト(名前が夜の夜景っぽい…)を使って似たようなことが行われてきましたが、やはりAS3のパッケージ空間や名前空間のように専門の機能と完全互換とはいかないので過度に使うのをやめています。
基本構文
それではさっそくCoffeeScriptを書いていきたいと思います。
ただし、この記事ではCoffeeScriptの文法を網羅するわけではありません。自分が「いつもAS3で書いてるあれってCoffeeScriptではどうやって書くのかな?」と調べ直す機会が多いものを抜き出してまとめているだけです。
CoffeeScriptについてよく知りたいときは、ぜひ最初に紹介した公式サイトかOreilly社の本を読むことをおすすめします。
関数
タブでそれぞれAS3、CoffeeScript、JavaScriptのコードに切り替わります。
//関数
var hello = function(){
trace('hello world');
};
//引数
var show = function(name:String){
trace('My name is' + name + '!!!!!');
};
//デフォルト引数
var total = function(price:Number, tax:Number = 1.05){
return price * tax;
};
//可変長引数
var cue = function(thread:Array, ...commands){
for each(var com:* in commands){
if (!~thread.indexOf(com)){
thread.push(com);
}
}
};
//無名関数を即座に実行
(function(){
trace('hello world');
})();
#関数
hello = ->
console.log 'hello world'
#引数
show = (name) ->
console.log "My name is #{name}!!!!!"
#デフォルト引数
total = (price, tax = 1.05) ->
price * tax
#可変長引数
cue = (thread, commands...)->
for com in commands
thread.push com if not (com in thread)
#無名関数を即座に実行
do ->
console.log 'hello world'
//関数
hello = function() {
return console.log('hello world');
};
//引数
show = function(name) {
return console.log("My name is " + name + "!!!!!");
};
//デフォルト引数
total = function(price, tax) {
if (tax == null) {
tax = 1.05;
}
return price * tax;
};
//可変長引数
cue = function() {
var com, commands, thread, _i, _len, _results;
thread = arguments[0], commands = 2 = 0)) {
_results.push(thread.push(com));
} else {
_results.push(void 0);
}
}
return _results;
};
//無名関数を即座に実行
(function() {
return console.log('hello world');
})();
いちおうこのコードの可変長引数のところにも混ぜていますが、CoffeeScriptのinは、配列のindexOfのIEサポートなんかしてたりして、地味ですがそうとう工数が減らせる便利な機能です。
バインディング
次のJavaScriptをみてください。
var Person = (function() {
//コンストラクター
function Person(name) {
this.name = name;
}
//名前をコンソールに表示
Person.prototype.say = function() {
console.log(this.name);
};
return Person;
})();
class Person{
private var name:String;
//コンストラクター
function Person(name:String):void{
this.name = name;
}
//名前をトレース
public function say():void{
trace(this.name);
}
}
このPersonクラスは一見すると正しく動くように思えます。参考までにAS3でも同じものを書きました。
しかしJavaScriptを次のように実行してみてください。jQueryを使っています。
$(function() {
var taro = new Person('太郎');
//ボタンがクリックされたら実行
$('#button').click(taro.say);
});
コンソールに「太郎」と表示されることを期待して書いたはずですが、表示されません。
この場合、(new Person).sayメソッド内で使われているthisはPersonインスタンスではなく、$(‘#button’)を指すようになるためです。よく言われるthisのスコープの仕様です。
特にコールバック関数内の処理で起こるので、
var self = this;
このようにコールバックの外で一旦thisを別の変数にバインディングしておくというセオリーを今まではとってきました。
CoffeeScriptではこれを次のような方法で解決してくれます。
class Person
constructor:(@name)->
say:=>
console.log @name
$ ->
taro = new Person '太郎'
$('#button').click taro.say
var Person,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
Person = (function() {
function Person(name) {
this.name = name;
this.say = __bind(this.say, this);
}
Person.prototype.say = function() {
return console.log(this.name);
};
return Person;
})();
$(function() {
var taro;
taro = new Person('太郎');
return $('#button').click(taro.say);
});
関数宣言の時につかう -> のかわりに => をつかうと、thisが常に現在のインスタンスを指すという機能です。個人的にこれかなりよく使います。
サンプルではクラスの中のインスタンスメソッドでバインドさせていますが、普通のコールバック関数の中やObjectオブジェクトの中の関数などいろんなところで使えるので、thisのバインドに困ったらこれです。
存在確認
あとCoffeeScriptの存在確認がとても便利でよく使うので、慣れるまでリファレンス引きなおすの面倒なのでこちらにまとめておきます。
#下の二行はどっちも同じ if user? then show(user) show(user) if user?
if (typeof user !== "undefined" && user !== null) {
show(user);
}
連続して書くことで存在を確認しながら処理を行えます。
user?.lover()?.together?(MerryChristmas)
var _ref;
if (typeof user !== "undefined" && user !== null) {
if ((_ref = user.lover()) != null) {
if (typeof _ref.together === "function") {
_ref.together(MerryChristmas);
}
}
}
あと、O’reilly本なんかではこんな書きかたもOKよと書かれてあるのですが、
jp_utweb_namespace ?= {}
今現在(2012/12/04)これをコンパイルするとエラーになります。ハテサテ?
今のところこんな感じ?なんか釈然としないです。
window.jp_utweb_namespace ?= {}
@jp_utweb_namespace ?= {}
クラス
クラスについて一気に書きます。
class User{
//コンストラクター
function User(name){
this.name = name;
}
//インスタンス変数
public var status;
public var name;
//インスタンスメソッド
public function clone(){
return new User(this.name);
}
//クラス変数
public static var active = 'active';
//クラスメソッド
public static function find(users, name){
var user, i, len;
len = users.length;
for(i = 0; i < len; i++){
user = users[i];
if(user.name === name){
return user.clone();
}
}
return null;
}
}
//継承
class Friend extends User{
//コンストラクター
function Friend(name){
super(name)
}
//オーバーライド
override public function clone(){
return new Friend(this.name)
}
}
class User
#コンストラクター
constructor:(@name)->
#インスタンス変数
status:false
#インスタンスメソッド
clone:->
new User(@name)
#クラス変数
@active:'active'
#クラスメソッド
@find:(users, name) ->
for user in users
if user.name is name then return user.clone()
return null
#継承
class Friend extends User
#コンストラクター
constructor:(name)->
super(name)
#オーバーライド
clone:->
new Friend(@name)
var Friend, User,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
User = (function() {
function User(name) {
this.name = name;
}
User.prototype.status = false;
User.prototype.clone = function() {
return new User(this.name);
};
User.active = 'active';
User.find = function(users, name) {
var user, _i, _len;
for (_i = 0, _len = users.length; _i < _len; _i++) {
user = users[_i];
if (user.name === name) {
return user.clone();
}
}
return null;
};
return User;
})();
Friend = (function(_super) {
__extends(Friend, _super);
function Friend(name) {
Friend.__super__.constructor.call(this, name);
}
Friend.prototype.clone = function() {
return new Friend(this.name);
};
return Friend;
})(User);
クラスに関してはこんなもんでしょうか。
いちおうprivate staticも。
class Creature{
//クラス変数
public static const LEFT= 'left';
public static const RIGHT = 'right';
//インスタンスメソッド
public function move(direction){
switch(direction){
case LEFT:
_moveLeft();
break;
case RIGHT:
_moveRight();
break;
default:
break;
}
}
//private変数
private static var _dist = 10;
//privateメソッド
private static function _moveLeft(){
x -= _dist;
}
private static function _moveRight(){
x += _dist;
}
}
class Creature
#クラス変数
@left:'left'
@right:'right'
#インスタンスメソッド
move:(direction)->
_moveLeft() if direction is User.left
_moveRight() if direction is User.right
true
#private変数もどき
_dist = 10
#privateメソッドもどき
_moveLeft = ->
@x -= _dist
_moveRight = ->
@x += _dist
var Creature;
Creature = (function() {
var _dist, _moveLeft, _moveRight;
function Creature() {}
Creature.left = 'left';
Creature.right = 'right';
Creature.prototype.move = function(direction) {
if (direction === User.left) {
_moveLeft();
}
if (direction === User.right) {
_moveRight();
}
return true;
};
_dist = 10;
_moveLeft = function() {
return this.x -= _dist;
};
_moveRight = function() {
return this.x += _dist;
};
return Creature;
})();
一見するとこれはうまく動いているようですが、ウンココードです。私が悪いんですが。
AS3のほうをよくみてもらうとわかるのですが、privateな変数と関数にはstaticがついています。私はあまりstaticを意識しないで書きましたが、たとえば
class Creature
#クラス変数
@left:'left'
@right:'right'
#インスタンスメソッド
move:(direction)->
_moveLeft() if direction is User.left
_moveRight() if direction is User.right
true
#private変数もどき
_dist = 10
#privateメソッドもどき
_moveLeft = ->
@x -= _dist
_moveRight = ->
@x += _dist
#新たに追加
change:->
_dist = 500
こんなふうにchangeメソッドを追加したとします。
a = new Creature() b = new Creature() c = new Creature() a.move(Creature.left) a.change() #ここから先、みんなもの凄いスピードで動く b.move(Creature.left) c.move(Creature.left)
そしてこんなふうに一つのインスタンスが値を変えてしまったら、他のインスタンス全部に影響が出てしまいます。
結局、原則としてprivateは使用しないことに決めました。あくまで原則として、なのでprivate staticが便利な場合、たとえば次に試すシングルトンでは使ったりもします。
シングルトンを書いてみる
どうしてもコンストラクターがアクセスできてしまうので、だいぶ苦労しました。
package {
public class Singleton {
private static var _instance:Singleton;
public static function getInstance():Singleton{
if(!Singleton._instance){
Singleton._instance = new Singleton(new Enforcer)
}
return Singleton._instance
}
public function Singleton(enforcer:Enforcer) {}
}
}
class Enforcer{}
class Singleton
_instance = null
@getInstance : ->
_instance = new _Singleton() if not _instance?
_instance
class _Singleton
constructor:->
properties:{}
set:(key, value)->
@properties[key] = value
get:(key)->
@properties[key]
var Singleton, _Singleton;
Singleton = (function() {
var _instance;
function Singleton() {}
_instance = null;
Singleton.getInstance = function() {
if (!(_instance != null)) {
_instance = new _Singleton();
}
return _instance;
};
return Singleton;
})();
_Singleton = (function() {
function _Singleton() {}
_Singleton.prototype.properties = {};
_Singleton.prototype.set = function(key, value) {
return this.properties[key] = value;
};
_Singleton.prototype.get = function(key) {
return this.properties[key];
};
return _Singleton;
})();
AS3のほうも、コンストラクターにgetInstanceとかからしかアクセス出来ないクラスの型を引数で渡すことを必須にすることでコンストラクターからのインスタンス生成を禁止しているという、力業といえばかなりの力業なんですが、CoffeeScriptはもう力業以前の問題で、コンストラクターへのアクセス禁止を完全にあきらめています。
いちおうSingleton.getInstance()からのインスタンスを取得する限りでは必ず一つであることが保証されますが、もっと研究する必要はありそうです。
さいごに
こんなかんじで、しばらくは出来ないところをなんとかして出来るような方法を探ってみたり代替方法を探してみたりしてたんですが、今のところは出来んなら出来んで割り切って使っていくといい感じに案件投入できました。そのうちコーディング規約とかも固まってきてそれで対処できそうだし。
何よりうれしかったのが、自分が素で書くJavaScriptのコードよりもだいぶきれいになったことです。
私は次はTypeScriptを触ってみたいと思います。EdgeとかCreateJSとかもはよ触りたくてうずうずしてるんですが、好きなんですよ。メタ言語が。
コードをコンパイルするときれいなコードが生まれる。すごいいいですよね。儲からなくていいから一生きれいなコードに囲まれて仕事したい。