javaScriptのクラスの構文はES6から大きく変わって便利になりました。ES6の構文だけ覚えたいところですが、旧構文から覚えることでjavaScriptならではのプロトタイプベースのオブジェクト指向の理解が深まります。本記事ではES6より前の旧構文のオブジェクト指向をまとめます。
クラスの定義
// クラス定義
function Norimono() {}
// クラスの定義 変数に代入してもOK
var Norimono = function() {};
javaScriptのクラスはただの関数と同等です。他の言語のようなクラスというものは存在せず、関数をクラスのように扱っているだけです。
インスタンスの生成
// クラス定義
function Norimono() {}
// インスタンスの生成
var norimono = new Norimono();
console.log(norimono);
Norimono {}
new
構文を使ってNorimonoクラスのインスタンスが生成できました。ためしにインスタンスをコンソール出力してみると、関数の文字列表現が出ました。
プロパティの定義
乗り物なのでプロパティとして定員を持たせることにします。
// クラス定義
function Norimono(capacity) {
this.capacity = capacity;
}
// インスタンスの生成
var norimono1 = new Norimono(10);
var norimono2 = new Norimono(50);
console.log(norimono1);
console.log(norimono2);
Norimono { capacity: 10 } Norimono { capacity: 50 }
メソッドの定義
定員が何人か教えてくれるsayCapacityメソッドを定義します。
// クラス定義
function Norimono(capacity) {
this.capacity = capacity;
this.sayCapacity = function() {
console.log('定員は' + capacity + '人です');
}
}
// インスタンスの生成
var norimono1 = new Norimono(10);
var norimono2 = new Norimono(50);
norimono1.sayCapacity();
norimono2.sayCapacity();
定員は10人です 定員は50人です
メソッドはprototypeで定義する
さっきのコードをリファクタリングします。メソッドはprototypeという領域に定義した方が、効率が良いのです。
// クラス定義
function Norimono(capacity) {
this.capacity = capacity;
}
// メソッドはprototypeで定義
Norimono.prototype.sayCapacity = function() {
console.log('定員は' + this.capacity + '人です');
}
// インスタンスの生成
var norimono1 = new Norimono(10);
var norimono2 = new Norimono(50);
norimono1.sayCapacity();
norimono2.sayCapacity();
定員は10人です 定員は50人です
すべてのインスタンスのコピー元となるテンプレートです。Object.prototype
に定義されています。prototypeを利用することで、メソッドの関数リテラルをインスタンス毎に生成する必要がなくなり、使用メモリを削減することができます。
prototypeで定義したメソッドからプロパティを参照する場合は、this.capacity
のようにthis
が必要ですのでご注意ください。this
は記述場所によって意味が変わりますが、ここではインスタンス自身を指します。
静的プロパティ、静的メソッド(クラスメソッド)
クラスで唯一の設定で良いものは、静的プロパティ、静的メソッドで定義しましょう。
通常のプロパティやメソッドは、インスタンス毎に値や結果が異なります。
例えば上記の例だと、capacityプロパティはインスタンス毎に10や50が設定されていました。
sayCapacityメソッドを実行するとインスタンス毎に出力される結果が変わりました。
これをインスタンスプロパティ、インスタンスメソッドと呼びます。
それに対して、インスタンス毎に値を持たずに、クラスで唯一の値で良いものは静的プロパティに設定します。静的メソッドも同様で、インスタンスプロパティを参照しないメソッドはすべて静的メソッドにすることが可能です。
※静的メソッドのことを「クラスメソッド」や「staticメソッド」と呼ぶこともあります。
// クラス定義
function Norimono() {}
// 静的プロパティ
Norimono.message = '発進しまーす';
// 静的メソッド
Norimono.sayMessage = function() {
console.log(this.message);
}
Norimono.sayMessage();
発進しまーす
公開範囲はpublicになる
静的プロパティ、静的メソッドはpublicになります。以下のコード例の通り、クラスの外部からも静的プロパティを参照できます。
// クラス定義
function Norimono() {}
// 静的プロパティ
Norimono.message = '発進しまーす';
console.log(Norimono.message);
発進しまーす
privateな静的プロパティ、静的メソッドを設定する方法はあるのだろうか…?
インスタンス経由で参照すると?
インスタンス経由で静的メソッドを呼ぼうとするとエラーになります。
// クラス定義
function Norimono() {}
// 静的プロパティ
Norimono.message = '発進しまーす';
// 静的メソッド
Norimono.sayMessage = function() {
console.log(this.message);
}
var norimono = new Norimono();
norimono.sayMessage();
TypeError: norimono.sayMessage is not a function
⬇インスタンスメソッド内部から静的メソッドを参照することは可能です。
// クラス定義
function Norimono() {
}
// 静的プロパティ
Norimono.message = '発進しまーす';
// 静的メソッド
Norimono.sayMessage = function() {
console.log(this.message);
}
Norimono.prototype.callSayMessage = function() {
Norimono.sayMessage();
}
var norimono = new Norimono();
norimono.callSayMessage();
発進しまーす
継承
Norimonoクラスを継承したCarクラスを作ります。
function Norimono(capacity) {
this.capacity = capacity;
}
Norimono.prototype.sayCapacity = function() {
console.log('定員は' + this.capacity + '人です');
}
// 子クラスの定義
function Car(capacity) {
// 親のコンストラクタの呼び出し
Norimono.call(this, capacity);
}
// 継承
Car.prototype = new Norimono();
var car1 = new Car(4);
var car2 = new Car(6);
car1.sayCapacity();
car2.sayCapacity();
定員は4人です 定員は6人です
子クラス独自のプロパティを定義する
Carクラスだけに車の種類のプロパティと、それをコンソール出力するメソッドを定義します。
function Norimono(capacity) {
this.capacity = capacity;
}
Norimono.prototype.sayCapacity = function() {
console.log('定員は' + this.capacity + '人です');
}
function Car(capacity, type) {
Norimono.call(this, capacity);
this.type = type;
}
Car.prototype = new Norimono();
Car.prototype.sayType = function() {
console.log('タイプは' + this.type + 'です');
}
var car1 = new Car(4, 'セダン');
var car2 = new Car(6, 'ワゴン');
car1.sayCapacity();
car1.sayType();
car2.sayCapacity();
car2.sayType();
定員は4人です タイプはセダンです 定員は6人です タイプはワゴンです
オーバーライドの方法
親クラスのメソッドを子クラスで上書きすることをオーバーライドといいます。Norimonoクラスで定義しているsayCapacityメソッドをオーバーライドしてみます。
/* 親クラスの定義**************************************/
function Norimono(capacity) {
this.capacity = capacity;
}
Norimono.prototype.sayCapacity = function() {
console.log('定員は' + this.capacity + '人です');
}
/* 子クラスの定義**************************************/
function Car(capacity, type) {
Norimono.call(this, capacity);
this.type = type;
}
Car.prototype = new Norimono();
Car.prototype.sayType = function() {
console.log('タイプは' + this.type + 'です');
}
// オーバーライド
Car.prototype.sayCapacity = function() {
console.log(this.type + 'の定員は' + this.capacity + '人だよーん');
}
/* インスタンス生成、実行***************************/
var car = new Car(4, 'セダン');
car.sayCapacity();
var norimono = new Norimono(100);
norimono.sayCapacity();
セダンの定員は4人だよーん 定員は100人です
NorimonoクラスとCarクラスで、sayCapacityメソッドの出力を変えることに成功しました。
privateを実現する仕組み
javaScriptには、厳密にはprivateを実現する仕組みはありませんが、クロージャを利用することでprivateっぽいことはできます。
privateメンバー
今まで説明してきたプロパティはpublicメンバーになっています。ためしに、クラス外部からcapacityプロパティを参照してみます。
function Norimono(capacity) {
this.capacity = capacity;
}
var norimono1 = new Norimono(10);
console.log(norimono1.capacity);
10
クラス外部からcapacityプロパティにアクセスできました。this
を使ってプロパティを設定するとpublicメンバーになるのです。
クラス外部からもcapacityを自由に変更できてしまうと思わぬ不具合につながりますので、このままでは安全性に欠けます。そこでcapacityプロパティを、クラス外部から参照できないようにprivateメンバーに変えます。オブジェクト指向の用語でいう「隠蔽化」や「カプセル化」ですね。
function Norimono(capacity) {
// privateメンバー
var _capacity = capacity;
}
var norimono1 = new Norimono(10);
console.log(norimono1._capacity);
undefined
_capacity
プロパティを参照できなくなりました!privateメンバーを定義するには、コンストラクター内で変数を宣言をするだけです。javaScriptの関数内のローカル変数のスコープは、関数内だけです。そのためクラス外部からはアクセスできなくなるのです。
javaの経験が長かった私としては「これがprivateメンバー…?単なるクロージャじゃん!」と言いたくなる仕様です。javaScriptでは関数の仕組みを利用して擬似的にprivateな仕組みを作っているだけのようです。
※変数名のプレフィックスに「_(アンダーバー)」を使うのはjavaScriptの慣例で、privateメンバーだとわかりやすくするためです。ただの命名ルールであり機能的な意味はありません。
privateメソッド
次にprivateメソッドを作ります。privateメンバーと同じで、コンストラクタ内に関数を定義するとprivateメンバーのようなものができます。
function Norimono() {
// privateメンバー
var _capacity;
// privateメソッド
function _validCapacity(capacity) {
return 0 < capacity;
}
// セッター
this.setCapacity = function(capacity) {
if (_validCapacity(capacity)) {
_capacity = capacity;
}
}
// ゲッター
this.getCapacity = function() {
return _capacity;
}
}
var norimono1 = new Norimono();
norimono1.setCapacity(10);
console.log(norimono1.getCapacity());
var norimono2 = new Norimono();
norimono2.setCapacity(-1);
console.log(norimono2.getCapacity());
10 undefined
たしかに実現できています。
⬇アクセサーメソッドをprototypeにできないか、やってみました。
function Norimono() {
// privateメンバー
var _capacity;
// privateメソッド
function _validCapacity(capacity) {
return 0 < capacity;
}
}
// prototypeでセッターを定義?
Norimono.prototype.setCapacity = function(capacity) {
if (this._validCapacity(capacity)) {
this._capacity = capacity;
}
}
var norimono1 = new Norimono();
norimono1.setCapacity(10);
TypeError: this._validCapacity is not a function
prototypeで定義したpublicメソッドからは、privateメソッドは参照できないようです。
- コンストラクター内で定義したpublicメソッド
→privateメソッドを参照可能 - prototypeで定義したpublicメソッド
→privateメソッドを参照不可能
少々わかりにくい仕様ですが、クロージャを利用した仕組みと考えると納得です。
ただ、ここまで説明しておいてなんですが、私はこれ使わないですね…。javaScriptの独自仕様なのでしっくりこないのと、javaScriptでprivateをどうしても使いたいときがないので。
感想
java歴が長い私にとっては、javaScriptの旧構文のオブジェクト指向はかなり強引な感じがあり、覚えにくいです。ES6からは文法としては改善しているようですが、中の仕組みはかわっていないので、旧構文は嫌でも理解しないとなぁと思いました。