How CoffeScript classes work
CoffeeScriptのクラス機構はどんなものか調べた記録。コード量は少ないが洗練されており効率も良いようだ。
http://coffeescript.org/#classes
さて、元のcoffeeスクリプトは単にクラスとサブクラスの定義をするだけのもの。
#!/usr/bin/env coffee class Animal constructor: (@name) -> console.log "constructor of Animal" move: (meters) -> console.log "Animal#move" class Snake extends Animal constructor: (@name) -> super @name console.log "constructor of Snake" move: -> console.log "Snake#move" super 5 sam = new Snake "Sammy the Python" sam.move()
これをcoffee -bc
で生成したJSをbeautifyしたものが以下である。
var Animal, Snake, sam, __hasProp = Object.prototype.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; }; Animal = (function () { function Animal(name) { this.name = name; console.log("constructor of Animal"); } Animal.prototype.move = function (meters) { return console.log("Animal#move"); }; return Animal; })(); Snake = (function (_super) { __extends(Snake, _super); function Snake(name) { this.name = name; Snake.__super__.constructor.call(this, this.name); console.log("constructor of Snake"); } Snake.prototype.move = function () { console.log("Snake#move"); return Snake.__super__.move.call(this, 5); }; return Snake; })(Animal); sam = new Snake("Sammy the Python"); sam.move();
まず、ベースクラスのないクラス(Animal)はJSネイティブでオブジェクトを定義する方法となんら代わりはない。functionでコンストラクタをつくり、prototypeにメソッドを入れるだけだ。
ベースクラスを持つクラス(Snake)は__extends()
というヘルパ関数を使って継承を実現している。__extends()
を少し書き換えたものが以下のもの。
// child/parentはそれぞれコンストラクタ関数 __extends = function (child, parent) { // クラスメソッドの継承 for (var key in parent) { if (parent.hasOwnProperty(key)) { child[key] = parent[key]; } } // プロトタイプチェインの構築 // parentを同じ性質をもつ中間代理クラス function ctor() { this.constructor = child; } // ctorのメソッド/プロパティはparentと同じ ctor.prototype = parent.prototype; // child.prototype = new parentに似ているが // parent()の呼び出しは伴わない child.prototype = new ctor; // 簡単に参照するためのエイリアス child.__super__ = parent.prototype; return child; };
クラスメソッドの継承はいいとして、プロトタイプチェインの構築がややこしい。Snake.prototypeはnew ctor
であり、ctor.prototypeはparent.prototypeである。つまりSnakeのインスタンスがプロトタイプチェインをたどるとき、Snake.prototype -> ctor instance -> ctor.prototype == parent.prototypeとなる。ctorのインスタンスが間にあるので、function ctorでセットしたプロパティがSnakeのインスタンスから参照できる。また、child.__super__はparent.prototypeの単なるエイリアスである。
この間にctorを挟むのは、child.prototypeを適切なオブジェクトに設定したいからである。child.prototypeは本来であればparentのインスタンスにしたいのだが、child.prototype = new parent()
とするわけにはいかない。parent()の引数が不明だし、副作用を伴うかもしれないからである。
それゆえに、parentのprototypeを参照しつつ安全にインスタンスを生成できる代理クラスであるctorを定義している。
さて、これを踏まえてSnakeのメソッドをみると、意味がわかる。まずコンストラクタ。
function Snake(name) { this.name = name; Snake.__super__.constructor.call(this, this.name); console.log("constructor of Snake"); }
Snake.__super__はparent.prototype、つまりAnimal.prototypeだ。Animal.prototype.constructorはAnimalそのものなので、このSnake.__super__.constructor.call(this, this.name);
という呼び出しは単にAnimal.call(this, this.name)
である。
次にインスタンスメソッド。
Snake.prototype.move = function () { console.log("Snake#move"); return Snake.__super__.move.call(this, 5); };
これもコンストラクタと読み方は変わらない。Snake.__super__.move.call(this, 5);
はAnimal.prototype.move.call(this, 5)
である。