JavaScriptでのprototypeを利用したクラスの継承

JavaScriptは、プロトタイプベースのオブジェクト指向言語であるため、クラスベースのプログラミング言語で使われるextendsのようなクラスを継承するための特別な構文はありませんが、prototypeを利用することにより、継承の機能を実現することができます。

prototypeを利用したクラスの継承

下記の例では、スーパークラスAnimalを継承したサブクラスDogを定義しています。まず、サブクラスのコンストラクタ内でcallメソッドを使ってスーパークラスのコンストラクタを呼び出します。

次にサブクラスのprototypeにスーパークラスのインスタンスを代入します。これで、サブクラスからスーパークラスのプロパティやメソッドにアクセスできるようになりますが、このままではコンストラクタ内のthisがスーパークラスを指してしまうので、サブクラスのprototype.constructorにサブクラスのコンストラクタを渡します。

これで、機能的には問題なく継承できているように見えますが、出力結果を見るとスーパークラスのコンストラクタが2回実行されていることが確認できます。これは、サブクラスのprototypeにスーパークラスのインスタンスを代入する際に呼んでいるコンストラクタが原因です。

JavaScript

(function(window) {
  function Animal(name) {
    console.log("Animal's constructor");
    this.name = name;
  }
  Animal.prototype.cry = function() {
    console.log('cry');
  };
  function Dog(name) {
    console.log("Dog's constructor");
    Animal.call(this, name);
  }
  Dog.prototype = new Animal();
  Dog.prototype.constructor = Dog;
  window.Animal = Animal;
  window.Dog = Dog;
}(window));

var dog = new Dog('Dog');
console.log(dog.name);
dog.cry();
console.log(dog instanceof Dog);
console.log(dog instanceof Animal);

/* 出力:
Animal's constructor
Dog's constructor
Animal's constructor
Dog
cry
true
true
*/

ダミークラスを利用した継承時のコンストラクタ重複問題の解決

クラスの継承時にコンストラクタが2回呼ばれてしまう問題の解決策として、まずダミークラスを使った方法を試してみます。下記の例では、ダミーのクラスを用意し、ダミークラスのprototypeにスーパークラスのprototypeを代入しておき、サブクラスのprototypeにはダミークラスのインスタンスを渡しています。出力結果を見るとスーパークラスのコンストラクタは1回しか呼ばれず、且つ継承の機能を実現できていることが確認できます。

JavaScript

(function(window) {
  function Animal(name) {
    console.log("Animal's constructor");
    this.name = name;
  }
  Animal.prototype.cry = function() {
    console.log('cry');
  };
  function Dog(name) {
    console.log("Dog's constructor");
    Animal.call(this, name);
  }
  var Dummy = function(){};
  Dummy.prototype = Animal.prototype;
  Dog.prototype = new Dummy();
  Dog.prototype.constructor = Dog;
  window.Animal = Animal;
  window.Dog = Dog;
}(window));

var dog = new Dog('Dog');
console.log(dog.name);
dog.cry();
console.log(dog instanceof Dog);
console.log(dog instanceof Animal);

/* 出力:
Dog's constructor
Animal's constructor
Dog
cry
true
true
*/

Object.createを利用した継承時のコンストラクタ重複問題の解決

次にObject.createを利用して、継承時のコンストラクタ重複問題を解決してみます。下記の例では、Object.create()でスーパークラスのprototypeをサブクラスのprototypeに代入しています。出力を見るとダミークラスを使った場合と同様にスーパークラスのコンストラクタは1回しか呼ばれず、且つ継承の機能を実現できていることが確認できます。なお、IE8以下のようなObject.createが使えない古いブラウザでは、自前でObject.createを定義するようにしています。

JavaScript

(function(window) {
  if (typeof Object.create !== 'function') {
    Object.create = function(o) {
      var F = function(){};
      F.prototype = o;
      return new F();
    };
  }
  function Animal(name) {
    console.log("Animal's constructor");
    this.name = name;
  }
  Animal.prototype.cry = function() {
    console.log('cry');
  };
  function Dog(name) {
    console.log("Dog's constructor");
    Animal.call(this, name);
  }
  Dog.prototype = Object.create(Animal.prototype);
  Dog.prototype.constructor = Dog;
  window.Animal = Animal;
  window.Dog = Dog;
}(window));

var dog = new Dog('Dog');
console.log(dog.name);
dog.cry();
console.log(dog instanceof Dog);
console.log(dog instanceof Animal);

/* 出力:
Dog's constructor
Animal's constructor
Dog
cry
true
true
*/

メソッドのオーバーライド

JavaScriptでは、オーバーライドのための特別な構文はありませんが、サブクラスでスーパークラスのメソッドと同じ名前のメソッドを定義するとオーバーライドすることができます。下記の例では、サブクラスでスーパークラスのメソッドをオーバーライドしていますが、実行してみるとサブクラスに定義したメソッドが実行されていることが確認できます。

JavaScript

(function(window) {
  if (typeof Object.create !== 'function') {
    Object.create = function(o) {
      var F = function(){};
      F.prototype = o;
      return new F();
    };
  }
  function Animal() {
  }
  Animal.prototype.cry = function() {
    console.log('cry');
  };
  function Dog() {
    Animal.call(this);
  }
  Dog.prototype = Object.create(Animal.prototype);
  Dog.prototype.constructor = Dog;
  Dog.prototype.cry = function() {
    console.log('bowwow');
  };
  window.Dog = Dog;
}(window));

var dog = new Dog();
dog.cry(); // 出力:bowwow

プロトタイプチェーン

JavaScriptでは、インスタンスのプロパティ(メソッドを含む)が参照された際、まずインスタンス自身のプロパティを探し、見つからなければprototypeプロパティを探します。それでも見つからなければ、prototypeのprototypeプロパティといった具合に最上位のObjectオブジェクトまで連鎖的に探し続けます。このprototypeを連鎖的に辿る継承のメカニズムをプロトタイプチェーンといいます。

下記の例では、Animalクラスを継承したDogクラスを定義し、さらにDogクラスを継承したPoodleクラスを定義して、それぞれのクラスのprototypeに同じ名前のメソッドを定義しています。そして、小クラスから順にprototypeに定義したメソッドをdeleteで削除していますが、削除後にメソッドを実行すると親クラスのメソッドが実行されていることがわかります。

JavaScript

(function(window) {
  if (typeof Object.create !== 'function') {
    Object.create = function(o) {
      var F = function(){};
      F.prototype = o;
      return new F();
    };
  }
  function Animal(name) {
    this.name = name;
  }
  Animal.prototype.cry = function() {
    console.log('cry');
  };
  function Dog(name) {
    Animal.call(this, name);
  }
  Dog.prototype = Object.create(Animal.prototype);
  Dog.prototype.constructor = Dog;
  Dog.prototype.cry = function() {
    console.log('bowwow');
  };
  function Poodle(name) {
    Dog.call(this, name);
  }
  Poodle.prototype = Object.create(Dog.prototype);
  Poodle.prototype.constructor = Poodle;
  Poodle.prototype.cry = function() {
    console.log('whine');
  };
  window.Animal = Animal;
  window.Dog = Dog;
  window.Poodle = Poodle;
}(window));

var poodle = new Poodle('Poodle');
console.log(poodle.name); // 出力:Poodle
poodle.cry(); // 出力:whine
delete Poodle.prototype.cry;
poodle.cry(); // 出力:bowwow
delete Dog.prototype.cry;
poodle.cry(); // 出力:cry
delete Animal.prototype.cry;
poodle.cry(); // エラー

スーパークラスのprototypeの動的拡張

プロトタイプベースの特徴として、prototypeを動的に拡張できる点が挙げられます。下記の例では、サブクラスDogをインスタンス化した後でスーパークラスAnimalにメソッドを追加していますが、Dogクラスのインスタンスからも追加したメソッドを実行できることが確認できます。

JavaScript

(function(window) {
  if (typeof Object.create !== 'function') {
    Object.create = function(o) {
      var F = function(){};
      F.prototype = o;
      return new F();
    };
  }
  function Animal(name) {
    this.name = name;
  }
  function Dog(name) {
    Animal.call(this, name);
  }
  Dog.prototype = Object.create(Animal.prototype);
  Dog.prototype.constructor = Dog;
  function Cat(name) {
    Animal.call(this, name);
  }
  Cat.prototype = Object.create(Animal.prototype);
  Cat.prototype.constructor = Cat;
  window.Animal = Animal;
  window.Dog = Dog;
  window.Cat = Cat;
}(window));

var dog = new Dog('Dog');
Animal.prototype.greeting = function() {
  console.log("Hello, I'm " + this.name + ".");
};
var cat = new Cat('Cat');
dog.greeting(); // 出力:Hello, I'm Dog.
cat.greeting(); // 出力:Hello, I'm Cat.