下記のようなクラス継承のコードは…

class Pet {
    name: string;
    age: number;
    constructor(name: string) {
        this.name = name;
        this.age = 0;
    }
    countAge() {
        this.age += 1;
    }
}

class Cat extends Pet {
    sleep() {
        alert(this.name + " is sleeping.");
    }
}

var cat = new Cat("Cathy");
cat.countAge();
cat.sleep();
alert(cat.name + " is " + cat.age + " y.o.");

伝統的にはprototypeをコピーして継承をするのが、 JavaScript のやり方。 流派はあるが大体下記のような感じじゃないだろうか:

function Pet(name) {
    this.name = name;
    this.age = 0;
}
Pet.prototype.countAge = function () {
    this.age += 1;
}

function Cat(name) {
    Pet.apply(this, arguments);
}
Cat.prototype = $.extend(Cat.prototype, Pet.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.sleep = function () {
    alert(this.name + " is sleeping.");
}

var cat = new Cat("Cathy");
cat.countAge();
cat.sleep();
alert(cat.name + " is " + cat.age + " y.o.");

最近の JavaScript (ECMA Script 5) では、Object.create( O [, Properties] )というのがある。

The create function creates a new object with a specified prototype. When the create function is called, the following steps are taken:

  1. If Type(O) is not Object or Null throw a TypeError exception.
  2. Let obj be the result of creating a new object as if by the expression new Object() where Object is the standard built-in constructor with that name
  3. Set the [[Prototype]] internal property of obj to O.
  4. If the argument Properties is present and not undefined, add own properties to obj as if by calling the standard built-in function Object.defineProperties with arguments obj and Properties.
  5. Return obj.

new演算子に近いような用途を想定した関数である。 一方で、引数としては Object.defineProperties() と同じ形ということになっている。 要するに下記のように使って欲しいらしい。

var Cat = {
    age: 0,
    countAge: function () {
        this.age += 1;
    },
    sleep: function () {
        alert(this.name + " is sleeping.");
    }
}

var cat = Object.create(Cat, {name: {value: "Cathy"}});
cat.countAge();
cat.sleep();
alert(cat.name + " is " + cat.age + " y.o.");

なお cat.name = "Cathy"を改名可能なようにするためには、 {value: "Cathy", writable: true}としなければならない。

で、この流儀だと以下の問題がある。なんてこった。

  • 継承はいままでの伝統的な方法しかない。(extend()処理のループが必要)
  • コンストラクタがObject.create()に置き換わったせいでインスタンス生成時の処理を書く方法がなくなる。

そしてMDNのInheritance Revisitedというページに書かれた"正解"を基に書いた、 Object.create()を利用したクラス継承の方法は、下記だ。 結構冗長。

function Pet(name) {
    this.name = name;
    this.age = 0;
}
Pet.prototype = Object.create(Object.prototype, {
    name: {
        writable: true
    },
    age: {
        writable: true
    },
    constructor: {
        value: Pet,
        enumerable: false
    },
    countAge: {
        value: function () {
            this.age += 1;
        }
    }
});

function Cat(name) {
    Pet.apply(this, arguments);
}
Cat.prototype = Object.create(Pet.prototype, {
    constructor: {
        value: Cat,
        enumerable: false
    },
    sleep: {
        value: function () {
            alert(this.name + " is sleeping.");
        }
    }
});

var cat = new Cat("Cathy");
cat.countAge();
cat.sleep();
alert(cat.name + " is " + cat.age + " y.o.");

prototype をObject.create() するという業! また、メソッドが単なる関数オブジェクトとして扱うということで、 {value: function() {...} }を大量にタイプすることになるという ちょっと残念な感じだ。 $.extend()関数をネイティブ関数に置き換えたのにコードは かえって複雑になっているのではないか。

やはり複雑なクラス構文を書くには、ネイティブなJavaScriptではまだ物足りない。 (物足りないからこそ private 変数などと組み合わせて記事にしなかった。) まだまだライブラリやら transcompiler (CoffeeScript, TypeScript, etc...) なしでは複雑なのはきつそうである。

ちなみに最初に書いた class などと書いたコードは ECMA Script 6 ないし TypeScript となっていて、 JavaScript に変換すると下のようになる。

var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
var Pet = (function () {
    function Pet(name) {
        this.name = name;
        this.age = 0;
    }
    Pet.prototype.countAge = function () {
        this.age += 1;
    };
    return Pet;
})();
var Cat = (function (_super) {
    __extends(Cat, _super);
    function Cat() {
        _super.apply(this, arguments);
    }
    Cat.prototype.sleep = function () {
        alert(this.name + " is sleeping.");
    };
    return Cat;
})(Pet);
var cat = new Cat("Cathy");
cat.countAge();
cat.sleep();
alert(cat.name + " is " + cat.age + " y.o.");

参考文献