すぐに分かる CoffeeScript によるクラスの書き方
年末年始から CoffeeScript を積極的に使っているわけですが、ピュアな JavaScript よりもクラスの取り扱いがしやすくなっていることに一番の恩恵を感じています。すべての機能を使いこなせているわけではありませんが、何件かの業務を通じてフィールド(インスタンス変数)やプライベート関数など自分流のクラスの書き方が出来てきたので、ここに書き記しておくとします。
よくある CoffeeScript のクラス構造
CoffeeScript に関する書籍やブログエントリを見ると、だいたいこのようにクラスを紹介していることが多いです。
class ClassName @staticVar: 0 # 静的なプロパティ @staticFunc: -> # 静的な関数 # do something... constructor: (name)-> # コンストラクタ @name = name # インスタンスのプロパティ publicFunc: -> # パブリックな関数 # do something...
なんか項目が少ない気がします。これだけでもそこそこの機能は実装できますが、この実装だとクラス内の全てが外部から丸見えになってしまいます。どうせクラスを作るのであれば、クラス内部からの参照に限定したフィールドやプライベート変数も積極的に使いたいところです。ではどうすればよいでしょうか?
フィールド(インスタンス変数)
CoffeeScript には JavaScript と違ってvarを使った明確な変数宣言がありません *1。コンパイラがソースコード全体を上から順にチェックして、未確認の変数名を見つけると、そのスコープ内(関数など)の冒頭に変数宣言文を記述します。そのためコンストラクタやパブリック関数内に普通に書いただけでは、単なるローカル変数とみなされてしまうため、クラス内で使いまわすことが出来ません。
それならばクラスオブジェクトの直下に空っぽの変数なり初期値付きの変数なりを明示的に宣言しておけば良いということになります。
class ClassName _fieldVar = false # フィールドとして定義 constructor: -> _fieldVar = true # フィールドの値を変更 # do something... publicFunc: -> "Now status is #{_fieldVar}" # 他の関数からも参照出来る
myclass = new ClassName() console.log myclass.publicFunc() # Now status is true console.log myclass._fieldVar # undefined
こうすることでフィールドはクラス内で使いまわされ、外部からは参照できないようになります。
プライベート関数
クラスにおける処理の実態はなるべく外部からは見えないようにしたいものです。外部から呼び出せるパブリック関数には引数のセットやプライベート関数の呼び出しだけに留め、処理の実態をプライベート関数に集約することでデバッグ時にコードを追いかける範囲を自然と狭めることが可能です。
関数の定義自体はフィールドと基本的には同じですが、呼び出し方に一つクセがあります。
class ClassName _fieldVar = false constructor: -> _fieldVar = true # do something... privateFunc = (value)-> if _fieldVar msg = "Hello, #{value}!" else msg = "Goodbye, #{value}" msg publicFunc: (target)-> privateFunc.call @, target
プライベート関数の呼び出しは call メソッドを使い、第一引数にコンテキストとしてインスタンス自身である @(※ this)を指定します。第二引数以降はプライベート関数自体に渡したい引数を指定することが出来ます。 *2
myclass = new ClassName() console.log myclass.publicFunc() # Hello, world! console.log myclass._fieldVar # Uncaught TypeError:
プライベート関数として外部から呼び出せなくなっているのがわかります。
クラス単位でファイルを分割
アプリケーションの規模が大きくなるに連れてクラスの数も増えてくるわけですが、そうなるとクラス単位でファイルを分割したくなります。
しかし CoffeeScript は JavaScript に変換する際に一ファイル単位でコード全体を即時関数でラップして隠蔽してしまう仕様となっています。そのためこのままでは外部ファイルからクラスを参照することができません。対処法として window オブジェクトのプロパティとしてクラスを定義します。厳密に言うならば window オブジェクトはグローバルではないのですが、1ページにつき一つのオブジェクトなのでそこを利用するというわけです。
class window.ClassName _fieldVar = false ・・・
Ruby on Rails のアセットパイプライン や RequireJS を使えば複数あるファイルを一つに結合出来ますが、即時関数でラップされたもの同士が結合されるだけなので、やはりこの対応をする必要があります。
クラスの継承
ピュアな JavaScript を書いていた頃は全く使おうとも思いませんでしたが、CoffeeScript を使うようになったことで継承のハードルがグッと低くなりました。よくある例として Animal クラスを Dog クラスに継承させてみるとします。
Animal.coffee
class window.Animal constructor: (name) -> @name = name alive: -> false
Dog.coffee
class Dwindow.og extends Animal constructor: -> super("Dog") dead: -> not @alive()
ファイルを分割する場合は、クラス名を定義する際に window. をつけるのを忘れずに。
extends 継承するクラス名と書くだけで継承処理は完了します。Animal クラスで定義した alive 関数も Dog クラス内で問題なく呼び出すことが出来ます。
ちなみにこのソースコードを JavaScript にコンパイルすると、以下のようになります。
Animal.js
(function() { window.Animal = (function() { function Animal(name) { this.name = name; } Animal.prototype.alive = function() { return false; }; return Animal; })(); }).call(this);
Dog.js
(function() { var __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; }; window.Dog = (function(_super) { __extends(Dog, _super); function Dog() { Dog.__super__.constructor.call(this, "Dog"); } Dog.prototype.dead = function() { return !this.alive(); }; return Dog; })(Animal); }).call(this);
どうですか奥さん。ずいぶんと面倒くさいコードが出てきました。これを全てタイプするのは流石に骨が折れるでしょう。CoffeeScript サマサマです。
クラス継承の使いどころ
実際の開発において Animal や Dog といったクラスを定義することは滅多にないかと思いますが、僕はイベントオブザーバー機能をクラスとして作成し、それを様々なクラスで継承して使うといった用途に使っていました。イベントオブザーバーに関しては、別のエントリで触れたいと思います。
山田流 CoffeeScript ひな形
CoffeeScript でクラスを作る場合、基本的に僕はこのひな形に沿って作るようにしています。
class window.ClassName extends SuperClass # Static variables @staticVar = 0 # fields _fieldVar_1 = false _fieldVar_2 = '' _fieldVar_3 = null # constructor constructor: (args)-> _fieldVar_1 = true _fieldVar_2 = args.foo _fieldVar_3 = args.bar # do something... # private methods privateFunc = (value)-> if _fieldVar msg = "Hello, #{value}!" else msg = "Goodbye, #{value}" msg # public methods publicFunc: (target)-> privateFunc.call @, target
コンストラクタ時に渡す引数はハッシュ型にします。そのため、インスタンス変数を以下のように渡すことができなくなりますが、引数をずらずら〜っと羅列するのは好みではないので、ハッシュでまとめて渡すようにします。
class window.ClassName # 引数名に @ をつけることで自動的にインスタンス変数に置き換えることができる constructor: (@name) -> ・・・ myclass = new ClassName('wakamsha')
脚注
- 厳密に言うならば、JavaScript においても var を使って変数を明確に宣言しなくとも、いきなり変数を記述して使うことは可能です。その場合 JavaScript はグローバル変数とみなして内部で自動的に処理します。そのため、お作法としては NG になります。 ↩
- この辺りは JavaScript の call() や apply() を理解しておくことをおすすめします。Function.prototype.apply() - JavaScript | MDN ↩