1. はじめに
オブジェクト指向言語のJavaScriptで効率的なプログラミングを行う際、ある「型枠」を作り、そこから「実体」を生成する手法が用いられることが多いと思います。通常、この「型枠」を「クラス」、そして「実体」を「インスタンス」と表現します。このブログでは、クラスの構造を説明した後、最終的にはクラスの継承がもっと身近なものになるように解説していきたいと思います。
ところで、学術的には、上記説明は「クラスベースのオブジェクト指向プログラミング言語」に当てはまります。一方、JavaScriptは「プロトタイプベースのオブジェクト指向プログラミング言語」に分類されます。「プロトタイプベース」においては、「型枠(クラス)」を「原型(プロトタイプ)」と呼ぶのが正確かもしれません。しかしながら、「プロトタイプベース」であっても、クラス同様の機能を実現できること、また、本ブログの趣旨は実践的な理解となりますので、タイトルはあえて「クラス」を用いることにします。
もちろん、「プロトタイプベース」である以上、「クラス」の機能を支える仕組みとして「プロトタイプ」は存在しますので、本ブログでも何度も取り上げることになります。ちなみに、ECMAScript 2015以降のJavaScriptでは「クラス構文」自体が存在しますが、これを支えているのは「プロトタイプベース」です。
それでは、実際に「neqto.js」を使用して詳しく解説していきます。「neqto.js」は、NEQTO Engine上で動作するJavaScript環境です。ECMAScript 5.1 Editionに対応しています。
2. クラスの定義
クラスの定義は関数そのものです。これは、クラスの「インスタンス」を形作る「コンストラクタ」として機能します。コンストラクタは、インスタンスを生成し、プロパティの追加、および次の重要な機能があります。
インスタンスにはその原型(プロトタイプ)へのリンクが設定されています。リンク先もまたインスタンスであり、これら一連のリンクがいわば鎖のように繋がっています。この概念が「プロトタイプチェーン」です。そして、コンストラクタにはインスタンスにプロトタイプチェーンを設定する役割があります。
以後、このコンストラクタの関数名が、クラスの名称です。また、クラスの継承について、継承元となるクラスを「スーパークラス」、継承先となるクラスを「サブクラス」として説明します。
コンストラクタ
以下は、クラス(Animal
)のコンストラクタです。この中で使用されるthis
は後述するnew
演算子付きで実行した際、暗黙的に生成されるオブジェクト(インスタンス)を指します。
コンストラクタ内に記述されたthis
付きプロパティは、生成されるインスタンスのプロパティとなります。インスタンス毎に存在する固有情報を持つ場合に適しています。一方、コンストラクタに直接追加したプロパティは、「クラスプロパティ」と呼ばれ、インスタンスに依存しない個別情報を付加したい場合に適しています。
print('--- Super Class ---');
var Animal = function(type) {
if(type)
this.type = type;
else
this.type = Animal.DEF_TYPE;
};
Animal.DEF_TYPE = 'lion';
クラスメソッド(静的メソッド)
クラスプロパティでも触れましたが、Date.now()
などのようにインスタンスに依存しないメソッドを必要に応じて追加することができます。
ここでは、コンストラクタの引数を省略したときに設定される値(Animal.DEF_TYPE
)を確認するためのクラスメソッドを用意しました。
Animal.getDefaultType = function() {
return Animal.DEF_TYPE;
};
print(Animal.getDefaultType()); //lion
メソッド(インスタンスメソッド)
インスタンス化した後に使用可能となるメソッドを追加します。単に「メソッド」と呼ばれる場合は、通常こちらを指します。
ここでは、単純にプロパティ(type
)を読み出すメソッドを用意しました。
Animal.prototype.getType = function() {
return this.type;
};
以上で、クラスの完成です。
3. クラスの継承
当然ですが、同じものは再利用した方が効率が良く、保守性も優れています。クラスの継承はまさに再利用です。
ここからは、クラスの継承について実践的に確認していきます。以下二つのアプローチで、微妙なニュアンスの違いにあえてこだわって、「継承」を少しだけ深掘りしながら説明していきます。
① Animal
クラス(スーパークラス)を継承したDog
クラス(サブクラス)の作成
サブクラスのコンストラクタを用意します。
コンストラクタの中には、必要に応じてDog
クラス独自の新規プロパティを追加しておきます。
Animal
クラスをnew
演算子で生成したインスタンスをDog.prototype
プロパティ上に配置することで、Animal
クラスのtype
プロパティは 「dog」固定 になります。
{Function}.prototype
とは、インスタンスにプロトタイプチェーンを設定するためのプロパティです。
あえてコンストラクタに含めなかったtype
プロパティは、プロトタイプチェーン上(Dog.prototype.type
)からアクセスできることがわかります。
print('--- Sub Class 1 ---');
var Dog = function(name, breed) {
this.name = name;
this.breed = breed;
};
Dog.prototype = new Animal('dog');
print(Dog.prototype instanceof Animal); //true
print(Dog.prototype.constructor === Animal); //true
print(Dog.prototype.type); //dog
Dog.prototype.constructor = Dog;
print(Dog.prototype.constructor === Dog); //true
② Animal
クラス(スーパークラス)を継承したPet
クラス(サブクラス)の作成
サブクラスのコンストラクタを用意します。
ここでは、{Function}.call
メソッドを使用して、スーパークラスのコンストラクタを再利用するようにします。
コンストラクタの中には、必要に応じてPet
クラス独自の新規プロパティを追加しておきます。
Object.create
でAnimal.prototype
を引数として生成したインスタンスをPet.prototype
プロパティ上に配置することで、Animal
クラスのtype
プロパティは 可変になります。指定がない場合は「dog」を使用することにします。
Object.create
はnew
演算子と異なり、インスタンス生成時にコンストラクタを実行しないという特徴があります。
①との重要な違いは、Pet.prototype.type
からアクセスできないことです。type
プロパティは、プロトタイプチェーン上(Dog.prototype.type
)には存在せず、後述するインスタンス化を経てアクセス可能です。
print('--- Sub Class 2 ---');
var Pet = function(name, type) {
if(type)
Animal.call(this, type);
else
Animal.call(this, 'dog');
this.name = name;
}
Pet.prototype = Object.create(Animal.prototype);
print(Pet.prototype instanceof Animal); //true
print(Pet.prototype.constructor === Animal); //true
print(Pet.prototype.type); //undefined
Pet.prototype.constructor = Pet;
print(Pet.prototype.constructor === Pet); //true
オーバーライド
「オーバーライド」とは継承元の動作を継承先で変更することです。主にメソッドで使用されますが、プロパティでも同じことが言えます。
ここでは、プロトタイプチェーン上でのオーバーライドを確認するため、第三のクラスとしてCat
クラスを作成してみます。Dog
クラスと同様に、Animal
クラスを継承することも可能ですが、あえてDog
クラスを継承してみます。
print('--- Sub Class 3 ---');
var Cat = function(name, breed) {
Dog.call(this, name, breed);
};
Cat.prototype = Object.create(Dog.prototype);
Cat.prototype.constructor = Cat;
type
プロパティが「dog」のままです。プロトタイプチェーンをつたって「dog」にたどり着いていることがわかります。
print(Cat.prototype.type); //dog
print(Object.getPrototypeOf(Cat.prototype).type); //dog
print(Cat.prototype.type === Object.getPrototypeOf(Cat.prototype).type); //true
type
プロパティを「cat」に変更する必要があるので、オーバーライドを使用します。
Cat.prototype.type = 'cat';
type
プロパティは、Dog
クラスとは異なるものとなりました。また、この時点でDog
クラスのtype
プロパティは「dog」のまま影響を受けないことがわかります。
print(Cat.prototype.type); //cat
print(Object.getPrototypeOf(Cat.prototype).type); //dog
print(Cat.prototype.type === Object.getPrototypeOf(Cat.prototype).type); //false
ここでは、オーバーライドをプロパティで試しましたが、メソッドでも同様です。
4. クラスのインスタンス化
new
演算子
関数をコンストラクタとして実行します。つまり、新しいインスタンスを生成します。
このnew
を付け忘れると想定外の動作を引き起こす可能性がありますので注意が必要です。
まず、スーパークラスであるAnimal
クラスのインスタンス化を試してみます。
var obj = new Animal();
print(obj.getType()); //lion
print(obj.type); //lion
次は、二つのサブクラスになります。
① Dog
クラス
var dog = new Dog('hachi', 'akita');
print(dog.getType()); //dog
print(dog.type); //dog
print(dog.name + ' ' + dog.breed); //hachi akita
② Pet
クラス
var pet = new Pet('miko', 'cat');
print(pet.getType()); //cat
print(pet.type); //cat
print(pet.name); //miko
内部プロパティ[[Prototype]]
インスタンスには、内部プロパティとして[[Prototype]]
が存在します。コンストラクタのprototype
プロパティと同じ参照がインスタンス生成時に設定されます。これら二つの「プロトタイプ」の違いを明確に理解することは「継承」を理解する上で重要です。
Object.getPrototypeOf({instance})
は、プロトタイプチェーンを構成する内部プロパティ[[Prototype]]
を取得するメソッドです。つまり、インスタンスからその内部プロパティである[[Prototype]]
へのアクセスは特別であることを意味します。
- Notes:
- 同様の意味を持ち簡易にアクセス可能な非推奨プロパティである
{instance}.__proto__
はneqto.jsでは非サポートです。 [[Prototype]]
は、ECMAScript仕様書の表記に従っています。
それでは、Dog
クラスおよびPet
クラスを使用して確認してみます。
① Dog
クラス
[[Prototype]]
に直接触れて見ると、type
プロパティがプロトタイプチェーン上に存在していることが改めてわかります。また、name
プロパティはインスタンス自身のプロパティであることも確認できます。
print(Object.getPrototypeOf(dog).type); //dog
print(Object.getPrototypeOf(dog).name); //undefined
print(Object.getPrototypeOf(dog) === Dog.prototype); //true
上記において、{Function}.prototype
および、このインスタンスの[[Prototype]]
が同じであることを確認しました。
次に、インスタンス生成後でも{Function}.prototype
の変更を行えば、全てのインスタンスが変更されることを確認してみます。
print(dog.color); //undefined
Dog.prototype.color = 'white';
print(dog.color); //white
上記は、{Function}.prototype
配下のプロパティを追加/変更した場合ですが、下記のように{Function}.prototype
そのものを再設定することは混乱の元となりますので避けるべきです。
Dog.prototype = ...;
② Pet
クラス
①とは異なり、type
プロパティがname
プロパティと同じであり、プロトタイプチェーン上に存在していないことがわかります。
print(Object.getPrototypeOf(pet).type); //undefined
print(Object.getPrototypeOf(pet).name); //undefined
print(Object.getPrototypeOf(pet) === Pet.prototype); //true
インスタンス生成後の{Function}.prototype
配下のプロパティ変更については、Dog
クラスと同様の挙動です。
print(pet.color); //undefined
Pet.prototype.color = 'white';
print(pet.color); //white
5. サンプルコード
これまで紹介してきたサンプルコードをまとめて掲載します。
これらのコードはすべてを通して一連動作するようになっています。
print('--- Super Class ---');
//constructor
var Animal = function(type) {
if(type)
this.type = type;
else
this.type = Animal.DEF_TYPE;
};
//class property/method
Animal.DEF_TYPE = 'lion';
Animal.getDefaultType = function() {
return Animal.DEF_TYPE;
};
print(Animal.getDefaultType()); //lion
//instance method
Animal.prototype.getType = function() {
return this.type;
};
print('--- Sub Class 1 ---');
//constructor
var Dog = function(name, breed) {
this.name = name;
this.breed = breed;
};
//prototype property
Dog.prototype = new Animal('dog');
print(Dog.prototype instanceof Animal); //true
print(Dog.prototype.constructor === Animal); //true
print(Dog.prototype.type); //dog
Dog.prototype.constructor = Dog;
print(Dog.prototype.constructor === Dog); //true
print('--- Sub Class 2 ---');
//constructor
var Pet = function(name, type) {
if(type)
Animal.call(this, type);
else
Animal.call(this, 'dog');
this.name = name;
}
//prototype property
Pet.prototype = Object.create(Animal.prototype);
print(Pet.prototype instanceof Animal); //true
print(Pet.prototype.constructor === Animal); //true
print(Pet.prototype.type); //undefined
Pet.prototype.constructor = Pet;
print(Pet.prototype.constructor === Pet); //true
print('--- Sub Class 3 ---');
var Cat = function(name, breed) {
Dog.call(this, name, breed);
};
Cat.prototype = Object.create(Dog.prototype);
Cat.prototype.constructor = Cat;
print(Cat.prototype.type); //dog
print(Object.getPrototypeOf(Cat.prototype).type); //dog
print(Cat.prototype.type === Object.getPrototypeOf(Cat.prototype).type); //true
//override
Cat.prototype.type = 'cat';
print(Cat.prototype.type); //cat
print(Object.getPrototypeOf(Cat.prototype).type); //dog
print(Cat.prototype.type === Object.getPrototypeOf(Cat.prototype).type); //false
print('--- Instantiation ---');
//super class instance
var obj = new Animal();
print(obj.getType()); //lion
print(obj.type); //lion
//(1) instance
var dog = new Dog('hachi', 'akita');
print(dog.getType()); //dog
print(dog.type); //dog
print(dog.name + ' ' + dog.breed); //hachi akita
//(2) instance
var pet = new Pet('miko', 'cat');
print(pet.getType()); //cat
print(pet.type); //cat
print(pet.name); //miko
print('--- Internal Property ---');
//(1) prototype chain
print(Object.getPrototypeOf(dog).type); //dog
print(Object.getPrototypeOf(dog).name); //undefined
print(Object.getPrototypeOf(dog) === Dog.prototype); //true
//(1) add to prototype property
print(dog.color); //undefined
Dog.prototype.color = 'white';
print(dog.color); //white
//(2) prototype chain
print(Object.getPrototypeOf(pet).type); //undefined
print(Object.getPrototypeOf(pet).name); //undefined
print(Object.getPrototypeOf(pet) === Pet.prototype); //true
//(2) add to prototype property
print(pet.color); //undefined
Pet.prototype.color = 'white';
print(pet.color); //white
6. まとめ
いかがでしたか。「クラス」とりわけ「継承」を身近なものにすることが本ブログの目標でした。neqto.jsは柔軟であり、様々なコーディング手法を用いることができます。今回紹介した「クラス」を使いこなし、「継承」を効果的に使えるようになれば、より効率的でかつ保守性の高いプログラムの構築に役立ちます。ぜひご参考にしてください。