MoyaSystem

もやしです。

JavaScriptにクラスはありません - プロトタイプで継承編 -

前回の内容はこちら。
JavaScriptにクラスはありません - オブジェクトリテラルとコンストラクタパターン編 - - MoyaSystem

方法3: プロトタイプを拝借する

クラスのないJavaScriptに用意されている、オブジェクト間でコードを再利用する方法の一つがプロトタイプです。同一のプロトタイプを持つオブジェクトでは、プロトタイプに定義されているプロパティにアクセスすることができます。
以下のコードではObject.create関数を使って、共通のプロトタイプを持つオブジェクトを生成しています。第1引数はプロトタイプ、第2引数はオブジェクト固有のプロパティとなります。

    var Person = function Person(){};
    Person.prototype.sayName = function(){
    	console.log("I am " + this.name);
    };
    Person.prototype.sayHo = function(){
    	console.log("Ho!");
    };

    var Singer = function Singer(){};
    Singer.prototype = new Person(); // Personのプロトタイプを継承
    Singer.prototype.sing = function(){
    	console.log(this.lyric);
    };

    var John = Object.create(Person.prototype,{
    	name: {value: "John"},
    	lyric: {value: "Imagine there's no heaven..."}
    });
    John.sayName(); // "I am John"
//  John.sing(); // undefined error

    var Bob = Object.create(Person.prototype,{
    	name: {value: "Bob"}
    });
    Bob.sayName(); // "I am Bob"

    John.sayHo = function(){
    	console.log("HOOOOOOOOOOOOO");
    };
    John.sayHo(); // "HOOOOOOOOOOOOO"
    Bob.sayHo(); // "Ho!"

    var LadyGaga = Object.create(Singer.prototype,{
    	name: {value: "Gaga"},
    	lyric: {value: "lalalala lalalala"}
    });
    LadyGaga.sayName(); // "I am Gaga"
    LadyGaga.sayHo(); // "Ho!"
    LadyGaga.sing(); // "lalalala lalalala"

Personを空のオブジェクトとして定義し、共有したいプロパティはすべてPerson.prototypeに定義するのがキモです。LadyGagaはPersonオブジェクトのプロトタイプを拡張したSingerオブジェクトとして作成しています。Singer.prototypeにPersonオブジェクトを代入することでPerson.prototypeにもアクセスすることができるようになります。ここでPerson.prototypeを代入してしまうと、Singer.prototypeで拡張した要素にPersonオブジェクトもアクセスできるようになる(John, Bobもsingできるようになる)ため、間違えないように注意してください。
ということで、Personオブジェクトを継承したSingerオブジェクトを作ることができました!

もう一工夫

継承ができたところで、クラスベースのオブジェクト指向言語でいうところのstatic変数っぽいものを作ってみます。ここではPersonオブジェクトすべてに付与されるmyIdプロパティを作成し、Personオブジェクトが作成されるたびにインクリメントされるようにしてみます。

    function Person(){};
    Person.prototype.sayName = function(){
    	console.log("I am " + this.myName);
    }
    Person.prototype.sayId = function(){
    	console.log("My Id is " + this.myId);
    }

    // ファクトリ
    var PersonFactory = function(){
    	var nextId = 0; // static変数的なもの
	return function(name){
		nextId++;
		return Object.create(
		Person.prototype,{
			myName: {value: name},
			myId: {value: nextId}
	         }
	    );
	};
    };

    var pfactory = PersonFactory();

    var John = pfactory("John");
    var Bob = pfactory("Bob");
    var Mary = pfactory("Mary");

    John.sayName(); // I am John
    John.sayId(); // My Id is 1

    Bob.sayName(); // I am Bob
    Bob.sayId(); // My Id is 2

    Mary.sayName(); // I am Mary
    Mary.sayId(); // My Id is 3

オブジェクトやプロトタイプそのものにmyIdをもたせるのはうまくいかないので、ファクトリメソッドを作成してその中で管理するようにしました。pfactory関数が実行されるたびにnextIdがインクリメントされます。

気をつけるべきこと

継承に頼り過ぎない

オライリーJavaScriptパターン』でも述べられていますが、継承はコードを再利用するための一つの方法にすぎません。このコード再利用したいなーと思ったときに、どんな実装をすれば効果的に再利用できるのか、引き出しをたくさんもっていることが重要だと思います。

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法

パフォーマンスに気を配る

以下のリンク先でも触れられているように、プロトタイプを利用するのにはパフォーマンス改善という意味もあります。

例えば、新しくオブジェクト/クラスを作成する時、一般的にメソッドはオブジェクトのコンストラクタの中で定義するのではなく、オブジェクトのプロトタイプに結びつけるべきです。コンストラクタの中で定義してしまうと、コンストラクタが呼び出されるたびに (つまりオブジェクトが作成されるたびに) メソッドが再代入されてしまうことになるからです。


クロージャ - JavaScript | MDN