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)である。