クラスの作成と使用
概要
hifiveのクラスモジュールを使用すると、JavaScriptのprototypeやdefinePropertyなどの仕組みを意識せず、簡単にクラスを定義し、使用することができます。
このクラスモジュールを用いると、多くのクラスベースオブジェクト指向言語で可能な
- クラスの継承
- メソッド、アクセサ関数のオーバーライド
- クラス定義で記述していない変数へのアクセスの防止(エラーを発生させる)
などをJavaScriptで実現できます。
ただし、クラスモジュールは、JavaやC#などとまったく同等の記述能力を提供するものではありません。
(例えば、同じ関数名で引数の異なる複数の関数を定義する「オーバーロード」はできません。)
あくまで、JavaScript(厳密にいうと、IE11などでも利用できるECMAScript5)の言語仕様に対して無理のない範囲で、
継承やより堅牢な変数アクセスの機能などを提供するのが目的です。
クラスモジュールを使用する
クラスを定義する
hifiveのクラスモジュールを利用してクラスを定義するには、 h5.cls.RootClassのextendメソッドの引数にクラス定義のオブジェクトまたはクラス定義のオブジェクトを返すfunctionを渡します。クラス定義の詳細についてはこちらを参照してください。
return {
// クラス名(完全修飾名)
name: 'sample.SampleClass1',
// クラスのフィールド定義
field: {
_count: null
},
// クラスのプロパティ(アクセサ)定義
accessor: {
scale: null
},
// クラスのメソッド定義
method: {
// クラスのコンストラクタ
// クラス名のfunctionを設定します
// (functionの名前は設定しなくても動作しますが、設定すると、Chromeの開発者ツールなどで見たときに
// このクラスのインスタンスの型が正しく表示される(この場合"SampleClass1"と表示される)ので、設定することを推奨します。)
constructor: function SampleClass1(initialValue) {
// コンストラクタの先頭で、必ず、extend()にセットした関数の第1引数に渡される
// super_オブジェクトのconstructor関数をcall(this)で実行します。
// (引数の名前(super_)は任意の名前にすることが可能ですが、特に問題がなければ"super_"にすることを推奨します。)
// これにより親クラスのコンストラクタが呼び出されます。
// 注:this._super.call(this) では無いことに注意してください。
// また、第1引数には必ず"this"を渡す必要があることに注意してください。
super_.constructor.call(this);
this._count = initialValue;
this.scale = 1;
},
incrementAndGet: function () {
return ++this._count;
},
getValue: function () {
return this._count * this.scale;
}
}
};
});
上記コードのコメントにもありますが、以下の点に注意して下さい。
- nameにクラスの完全修飾名を設定します
- コンストラクタ(constructor)を必ず定義し、クラス名と同じ名前のfunctionを設定します
- コンストラクタの先頭で親クラスのコンストラクタを呼び出します(super_.constructor.call(this); )。
クラスをインスタンス化する
hifiveのクラスモジュールを使用して作成したクラスのインスタンスを取得するには対象のクラスのcreate staticメソッドを呼び出します(newでインスタンス化することはできません)。create staticメソッドの引数はインスタンス化するクラスのコンストラクタの引数と同じで、create staticメソッドの引数がそのままコンストラクタの引数に渡されます。
instance.incrementAndGet(); // 11
instance.getValue(); // 11
instance.scale = 2;
instance.getValue(); // 22
クラスを継承する
hifiveのクラスモジュールではクラスの継承がサポートされています。クラスを継承するには、継承元クラス(親クラス)のextendメソッドに対し、クラスの定義オブジェクトを返す関数を渡します。
(つまり普通のクラス定義と同じです。)
var ParentClass = h5.cls.RootClass.extend(function (super_) {
return {
name: 'sample.ParentClass',
field: {
_count: null
},
method: {
constructor: function ParentClass(initialValue) {
super_.constructor.call(this);
this._count = initialValue;
},
incrementAndGet: function () {
return ++this._count;
}
}
};
});
// ParentClassを継承したクラスを定義するには、ParentClass.extendを利用します
var ChildClass = ParentClass.extend(function (super_) {
return {
name: 'sample.ChildClass',
method: {
constructor: function ChildClass(initialValue) {
// 親クラスのコンストラクタを呼び出します
// 親クラスのコンストラクタに引数がある場合、callの引数に含めます
super_.constructor.call(this, initialValue);
},
getAndIncrement: function () {
// 親クラスのフィールドにアクセスできます
return this._count++;
}
}
};
});
// 子クラスを使用します
var instance = ChildClass.create(10);
var count1 = instance.getAndIncrement(); // 10
// 親クラスのメソッドを使用できます
var count2 = instance.incrementAndGet(); // 12
上記コメントにある通り、親クラスのコンストラクタに引数がある場合、 子クラスのコンストラクタの親コンストラクタ呼び出しを super_.constructor.call(this, arg1, arg2, arg3, ...) と記述します。このように記述することで親コンストラクタの引数に値が渡されます。
また、クラスモジュールのクラスの継承では子クラスから親クラスのフィールドやアクセサ、メソッドをthisで参照することができます。
クラスを公開する
hifiveのクラスモジュールではクラス定義のnameにクラスの完全修飾名を設定しています。しかし、自動で外部に(グローバル空間に)公開されることはありません。定義したクラスを外部で使用するには、明示的に公開をするか、クラスマネージャ(h5.cls.manager)からクラスを取得する必要があります。
h5.u.obj.exposeを使用したクラスの公開例を示します。
// 外部に公開するパブリッククラス
var PublicClass = h5.cls.RootClass.extend(function (super_) {
return {
name: 'ns1.ns2.PublicClass',
method: {
constructor: function PublicClass() {
super_.constructor.call(this);
}
}
};
});
// 外部に公開しないプライベートクラス
var PrivateClass = h5.cls.RootClass.extend(function (super_) {
return {
name: 'ns1.ns2.PrivateClass',
method: {
constructor: function PrivateClass() {
super_.constructor.call(this);
}
}
};
});
// hifiveのオブジェクト公開APIを利用して外部へ公開します
h5.u.obj.expose('ns1.ns2', {
PublicClass: PublicClass
});
})();
// PublicClassのインスタンスを生成します
var publicClassInstance = ns1.ns2.PublicClass.create();
// PrivateClassは公開されていないため、エラーになります
var privateClassInstance = ns1.ns2.PrivateClass.create()
またクラスマネージャを使用するとクラスをスコープ外でも取得できます。
クラスマネージャーは h5.cls.manager に公開されています。クラスマネージャーの getClass メソッドに取得したいクラスの完全修飾名を渡すことで、定義済みのクラスオブジェクトが取得できます。
// クラスを定義します
var PublicClass = h5.cls.RootClass.extend(function (super_) {
return {
name: 'ns1.ns2.PublicClass',
method: {
constructor: function PublicClass() {
super_.constructor.call(this);
}
}
};
});
// クラスを名前空間に公開しません
})();
// 定義したクラスを取得します
var PublicClass = h5.cls.manager.getClass('ns1.ns2.PublicClass');
var publicClassInstance = PublicClass.create();
クラス定義
概要
hifiveクラスモジュールのクラス定義とは、クラスの構造を表すオブジェクトです。クラス定義には以下の情報を記述できます。
- クラス名(完全修飾クラス名)
- クラスが抽象クラスかどうか
- クラスを動的に拡張可能かどうか
- クラスのコンストラクタ
- クラスのインスタンスフィールド
- クラスのインスタンスプロパティ(アクセサ)
- クラスのインスタンスメソッド
クラス定義の必須項目はクラス名、コンストラクタ、およびコンストラクタ関数内での親クラスのコンストラクタ呼び出しです。名前空間が ns1.ns2、クラス名が SampleClass のクラスでは、このようになります。
var descriptor = {
// クラスの完全修飾名(必須)
name: 'ns1.ns2.SampleClass',
method: {
// コンストラクタ(必須)
constructor: function SampleClass() {
// 親クラスのコンストラクタ呼び出し(必須)
super_.constructor.call(this);
}
}
};
return descriptor;
});
クラス名
クラス定義においてクラス名は、クラス定義オブジェクトのルート要素のnameプロパティに名前空間を含める完全修飾名を記述します。クラス名は必須項目です。
名前空間を持たないクラスの場合、クラス名を SampleClass とすると、このように記述します。
name: 'SampleClass'
};
名前空間があるクラスの場合、名前空間を ns1.ns2、クラス名を SampleClass とすると、以下の様になります。
name: 'ns1.ns2.SampleClass'
};
フィールド
クラス定義にはクラスが持つインスタンスフィールドが定義できます。フィールドの定義はクラス定義オブジェクトのルート要素のfieldプロパティに設定するオブジェクトに記述します。オブジェクトのキーがフィールド名、値がそのフィールドのフィールド定義となります。
フィールド定義は以下のプロパティを持ちます。
プロパティ名 | 型 | 説明 |
---|---|---|
defaultValue | any | インスタンス生成時に挿入されるフィールドのデフォルト値。 |
フィールドの定義例です。
// フィールド定義はルート要素のfieldプロパティ内に記述します
field: {
// デフォルト値があるフィールドです
// _messageフィールドにはインスタンス生成時に「Hello World!」が挿入されています。
_message: {
defaultValue: 'Hello World!'
},
// デフォルト値が無いフィールドです。
// _nameフィールドはインスタンス生成時にnullが挿入されています。
_name: null
}
};
プロパティ(アクセサ)
クラス定義にはクラスが持つインスタンスプロパティやアクセサが定義できます。プロパティ(アクセサ)の定義はクラス定義オブジェクトのルート要素のaccessorプロパティに設定するオブジェクトに記述します。オブジェクトのキーがプロパティ(アクセサ)名、値がそのプロパティ(アクセサ)の定義となります。
プロパティ(アクセサ)定義は以下のプロパティを持ちます。
プロパティ名 | 型 | 説明 |
---|---|---|
get | Function | プロパティのgetアクセサです。 |
set | Function | プロパティのsetアクセサです。 |
プロパティの定義例です。
field: {
_scale: {
defaultValue: 1
},
_value: {
defaultValue: 1
},
},
// プロパティ定義はルート要素のaccessorプロパティ内に記述します
accessor: {
// 「autoProperty」という名前のプロパティを作成します。
// プロパティ定義をnullとした場合、自動でバッキングフィールド及びget/setアクセサが生成されます。
// バッキングフィールドへは「this._p_ + プロパティ名」でアクセスできます(this._p_autoProperty)。
autoProperty: null,
// フィールドに対するアクセサ定義です。
// プロパティ名が「scale」のget及びsetアクセサが生成されます。
scale: {
get: function () {
return this._scale;
},
set: function (scale) {
this._scale = scale;
// 値セット以外の処理も記述できます。
this._value *= scale;
}
},
// フィールドに対する読み取り専用アクセサ定義です。
// プロパティ名が「value」のgetアクセサが生成されます。
value: {
get: function () {
return this._value;
}
}
}
};
メソッド
コンストラクタ
クラス定義においてコンストラクタは、クラス定義オブジェクトのルート要素のmethodプロパティのconstructorプロパティに記述します。コンストラクタは必須項目です。
コンストラクタにはクラス名と同じ名前の関数を設定します。また親クラスのコンストラクタを呼び出すためにsuper_.constructor関数をcallで実行します。
method: {
// コンストラクタを定義します(クラス名がSampleClassの場合)。
constructor: function SampleClass(arg1, arg2, ...) {
super_.constructor.call(this);
}
}
};
継承元のクラスのコンストラクタに引数がある場合、super_.constructor関数をcallするとき、第2引数以降に引数を追加します(第1引数は常に"this"を渡す必要があることに注意してください)。
method: {
// コンストラクタを定義します(クラス名がSampleClassの場合)。
// 親クラスのコンストラクタ引数が arg1、arg2 の2つの場合、以下の様に
// SampleClass._super.callに引数を追加します。
constructor: function SampleClass(arg1, arg2, ...) {
super_.constructor.call(this, arg1, arg2);
}
}
};
その他のメソッド
クラス定義にはクラスが持つインスタンスメソッドが定義できます。メソッドの定義はクラス定義オブジェクトのルート要素のmethodプロパティに設定するオブジェクトに記述します。オブジェクトのキーがメソッド名、値がそのメソッドの関数となります。
method: {
// コンストラクタを定義します(クラス名がSampleClassの場合)。
constructor: function SampleClass(arg1, arg2, ...) {
super_.constructor.call(this, arg1, arg2);
},
// moveメソッドを定義します。
move: function () {
// moveメソッドの処理内容を記述します。
}
}
};
その他
抽象クラス
hifiveクラスモジュールで生成するクラスを抽象クラスとすることができます。クラス定義オブジェクトのルート要素のisAbstractプロパティにtrueを設定すると抽象クラスとなり、インスタンスを生成しようとするとエラーが発生します。
var abstractClassDesc = {
name: 'AbstractClass',
// AbstractClassを抽象クラスとして設定します。
// 抽象クラスとしない場合は、isAbstractを記述する必要はありません。
isAbstract: true,
method: {
constructor: function AbstractClass() {
super_.constructor.call(this);
}
}
};
return abstractClassDesc;
):
var instance = AbstractClass.create(); // Uncaught Error: このクラスは抽象クラスです。インスタンスを生成することはできません。
クラスの動的拡張
hifiveクラスモジュールで生成したクラスは、デフォルトではインスタンスの動的な拡張ができません。
'use strict';
var SampleClass = h5.cls.RootClass.extend(function (super_) {
return {
name: 'SampleClass',
method: {
constructor: function SampleClass() {
super_.constructor.call(this);
}
}
};
});
var instance = SampleClass.create();
// 厳格モード(use strict)ではSampleClassには存在しないプロパティにアクセスするとエラーが発生します。
instance.unknownProperty = "Error"; // Uncaught TypeError: Can't add property unknownProperty, object is not extensible
})();
クラス定義には動的な拡張を可能にするisDynamicプロパティがあります。isDynamicにtrueを設定すると、上記のコードでもエラーが発生せず、プロパティも追加されます。
'use strict';
var SampleClass = h5.cls.RootClass.extend(function (super_) {
return {
name: 'SampleClass',
// 動的に拡張できるクラスとして定義する
isDynamic: true,
method: {
constructor: function SampleClass() {
super_.constructor.call(this);
}
}
};
});
var instance = SampleClass.create();
// エラーが発生せす、プロパティも追加されている。
instance.unknownProperty = "OK";
instance.unknownProperty; // "OK"
})();
TIPS
プライベートなstatic変数や関数を定義する
hifiveクラスモジュールでは、インスタンスのフィールドやメソッドのみがサポートされています。プライベートなstatic変数や関数はクラスを定義する際の関数スコープを利用することで、利用することができます。
次のコードは、生成するインスタンスに連番のIDを持たせるサンプルです。
// private static変数です。
var id = 0;
// private staticメソッドです。
// 連番のIDを生成して返します。
function createId() {
return ++id + '';
}
return {
name:'SampleClass',
accessor: {
id: null
},
method: {
constructor: function SampleClass() {
super_.constructor.call(this);
// クラスのidに連番を設定します。
this.id = createId();
}
}
};
});
var instance1 = SampleClass.create();
instance1.id; // "1"
var instance2 = SampleClass.create();
instance2.id; // "2"
クラスのインスタンスかどうか確認する
あるオブジェクトがhifiveクラスモジュールで生成したクラスのインスタンスであるかどうかを調べるには、クラスの isClassOf staticメソッドを使用します。
return {
name:'SampleClass',
method: {
constructor: function SampleClass() {
super_.constructor.call(this);
}
}
};
});
var instance = SampleClass.create();
// isClassOf staticメソッドを利用すると、対象が自身のインスタンスかどうかがチェックできます。
SampleClass.isClassOf(instance); // true
var object = {};
SampleClass.isClassOf(object); // false
親クラスを取得する
hifiveクラスモジュールではクラスの継承がサポートされています。子クラスの getParentClass staticメソッドを使用すると親クラスを取得することができます。
return {
name: 'ParentClass',
method: {
constructor: function ParentClass() {
super_.constructor.call(this);
}
}
};
});
var ChildClass = ParentClass.extend(function (super_) {
return {
name: 'ChildClass',
method: {
constructor: function ChildClass() {
super_.constructor.call(this);
}
}
};
});
ChildClass.getParentClass() === ParentClass; // true