はじめに
JavaScriptでオブジェクト指向言語の継承に相当する概念を実装する方法は、大きく分けて4つあります。実務上はライブラリを使ったり、TypeScriptを使ったりと、直接意識する必要があることは少ないわけですが、それだといつまでたっても「JavaScriptにおける継承」を理解できません。ES6で、シンタックスシュガーとしてのclass / extendも導入される可能性も高そうですが、そんな今だからこそ「本来はどう書くのか?」を整理してみることにしました。
- コンストラクタを使いprototypeチェーンを使って継承する
- コンストラクタは使うが、prototypeチェーンは使わず、prototypeを直接拡張する
- コンストラクタは使わず、prototypeチェーンを使って継承する
- コンストラクタもprototypeも使わず、直接オブジェクトを拡張する
JavaScriptという言語は、prototypeを使って継承するように作られていて、クラスを使って継承する言語ではありません。ただ、良く知られているようにコンストラクタ関数からnew演算子を使ってインスタンスを生成することはでき、この場合、クラスベースの言語の「クラス - インスタンス」と同等のことをすることはできます。そして、コンストラクタ(クラスベース言語のクラスに相当)を継承したコンストラクタを作ることで、クラスベース言語の継承と同等のことを行うこともできます。(1のパターン)
ただ、JavaScriptではオブジェクトを継承をするのに、コンストラクタは必須ではなく、オブジェクトから別のオブジェクトを直接継承することができるのです(3のパターン)。しかも、上述の1のパターンのおいて、コンストラクタ(のprototype)の継承は、3のパターンと同等の仕組みが使われているわけで、その意味で、JavaScriptの継承は基本的には3のパターンであり、1はそこから派生した機能と言うことができるのです。
1), 3)のようにprototypeチェーンを使うことのメリットは、継承先オブジェクト生成後に継承元オブジェクトを変更したときに、リアルタイムで継承したオブジェクトに反映されること、無駄なメモリを使わないことなどがあります。
ただし、この方法では直線的な継承しかできず、クラスベース言語の多重継承のようなことはできません。また、オブジェクトをHashMap的に使おうとした時に、prototypeの継承が含まれると、プロパティを列挙するのが面倒という問題があります。そこで、しばしばprototypeチェーンによる継承の代わりに、「拡張」を使う方法がとられます(
2と4のパターン)。ここで「拡張」というのは、JavaScriptの言語仕様上の用語ではないのですが、jQuery.extendやangular.extendのように、オブジェクトのプロパティをコピーする手法のことを言っています。サンプルプログラムでは、$.extendという記述にしていますが、他のライブラリのextendでも同じです。
ちなみに、プログラム中に出てくるobjectFromProtoの定義については、最後に説明します。
1) コンストラクタを使いprototypeチェーンを使ってクラス継承する
function inheritConstructor(Parent, Constructor, prop){
Parent = Parent || Object;
Constructor.prototype = $.extend(objectFromProto(Parent.prototype), prop, {
__super: Parent.prototype,
constructor: Constructor
});
return Constructor;
}
Child = inheritConstructor(Super, function Child(){}, {});
2) コンストラクタは使うが、prototypeチェーンは使わず、prototypeを直接拡張する
function extendConstructor(Parent, Constructor, prop){
var Parent = Parent || Object;
$.extend(Constructor.prototype, Parent.prototype, prop, {
__super: Parent.prototype,
constructor: Constructor
});
return Constructor;
}
Child = extendConstructor(Super, function Child(){}, {});
3) コンストラクタは使わず、prototypeチェーンを使って継承する
function inheritObject(parent, prop){
return $.extend(objectFromProto(parent), prop);
}
child = inheritObject(parent, {});
child = inheritObject(Parent.prototype, {});
4) コンストラクタもprototypeも使わず、直接オブジェクトを拡張する
function extendObject(parent, prop){
return $.extend({}, parent, prop);
}
child = extendObject(parent, {});
child = extendObject(Parent.prototype, {});
ちなみに、objectFromProtoは、以下のように定義されているものとします。
if(typeof Object.create === 'function'){
var objectFromProto = function(proto){
return Object.create(proto);
}
}else{
var objectFromProto = function(proto){
var Temp = function(){};
Temp.prototype = proto;
return new Temp();
}
}
この関数は、protoというオブジェクトがあったときに、x.proto = protoであるような新しいオブジェクトxを作るものです。xは、protoを継承し、x.aが独自に定義されていなければ、x.aで、proto.aにアクセスすることができます。JavaScriptの継承のコアとなる関数です。
今、ほとんどの処理系ではObject.createが定義されているので、これを使えば良いのですが、古い処理系では、コンストラクタを使った実装が使われます。一見してトリッキーな実装ですが、要するにJavaScriptでは、コンストラクタからのインスタンスの生成にも、prototypeチェーンが使われているわけであり、これを使った実装ということになります。クラスベース言語では、継承とインスタンス生成は全く違う仕組みであるわけですが、これが同じ仕組みで実現されているところが、JavaScriptの味わい深いところではないでしょうか。
余談ですが、ES6でclass / extendsキーワードが導入される予定です。これは本質的には、1)のシンタックスシュガーであり、このため、もはや1)や3)のパターンで「クラス」が使われていると言っても差し支えないと思うのですが、現状では混乱を招くため、JavaScriptにはクラスという概念はないものとして説明しています。