チェ・ゲバムラの日記

脱犬の道を目指す男のブログ

【JS】大規模開発でも通用する書き方を身につけるオブジェクト指向構文(prototype,クラスライクな継承)〜JavaScript本格入門 Chapter5〜

JavaScriptにおけるオブジェクト指向の特徴

クラスではなくプロトタイプ

他言語と違ってインスタンスの概念はあるものの、クラスがなくてプロトタイプという雛形の概念だけがある。
プロトタイプとは要するに「より縛りの弱いクラスのようなもの」と思って差し支えない。

最もシンプルなクラスを定義

var Member = function() {
}

これだけでクラス完成。

あとはインスタンス化するだけ。

var mem = new Member();

コンストラクタで初期化する

var Member = function(firstName,lastName){
    this.firstName = firstName;
    this.lastName = lastName;
    this.getName = function() {
        return this.lastName + ' ' + this.firstName;
    }
};

var mem = new Member('太郎','山田');
document.writeln(mem.getName()); //山田 太郎

プロパティ定義
this.プロパティ名 = 値;

動的にメソッドを追加する

var Member = function(firstName,lastName){
    this.firstName = firstName;
    this.lastName = lastName;
};

var mem = new Member('太郎','山田');
mem.getName = function() {
    return this.lastName + ' ' + this.firstName;
}

document.writeln(mem.getName()); //山田 太郎 この場合もgetNameは動作する。

var mem2 = new Member('奈美','掛谷');
document.writeln(mem2.getName()); //エラー!

mem2にgetNameはないのでエラー。
つまり同一のクラスを元に生成されたインスタンスでも、
それぞれが持つメンバは同一とは限らないので注意。
このようなゆるさが「より縛りの弱いクラスのようなもの」と言われるゆえんである。

コンストラクタの問題点

メソッドの数に比例して無駄なメモリを消費するのが問題。
コンストラクタは、インスタンスを生成するたびに、それぞれのインスタンスのためにメモリを確保する。
上述したgetNameでいえば全てのインスタンスで中身が同じなのでインスタンス単位でメモリを確保するのは無駄。

これを解決するのがprototypeというプロパティ。
prototypeを使うとインスタンスコピーしても、コピー元のメンバを見に行く=メモリ使用量を節約できる。

var Member = function(firstName,lastName){
    this.firstName = firstName;
    this.lastName = lastName;
};

Member.prototype.getName = function(){
    return this.lastName + ' ' + this.firstName;
};

var mem = new Member('太郎','山田');
document.writeln(mem.getName());//山田 太郎

プロトタイプオブジェクト利用するメリット2点

1.メモリ使用量節減
上述した通り。

2.メンバ追加や変更をインスタンスがリアルタイムに認識
インスタンスにメンバをコピーしないということは、
プロトタイプオブジェクトへの変更をインスタンス側で動的に認識できるということ。
つまり、上述のコードでgetNameメンバを追加=>インスタンス化しているところを、
インスタンス化=>getNameメンバ追加としても正常に動作する。

プロタイプオブジェクトの不思議(プロパティの設定)

プロトタイプオブジェクトでプロパティ宣言したら?
→値の参照時だけプロトタイプオブジェクトが利用される。

var Member = function(){
};
Member.prototype.sex = '男';

var mem1 = new Member();
var mem2 = new Member();
document.writeln(mem1.sex + '|' + mem2.sex); //男|男
mem2.sex = '女';
document.writeln(mem1.sex + '|' + mem2.sex); //男|女

最後は女|女とならず、インスタンスmem2の内容だけが書き換えられる。
つまり値の設定は常にインスタンスに対して行われる。
mem1のインスタンス自体にはsexプロパティがないので元のMemberクラスのデフォルトを見に行く。
mem2のインスタンスにはsexプロパティが設定されているので「女」と表示される。

※ただし、どうせインスタンス単位で値は異なるのが普通なので、このようにプロパティをプロトタイプオブジェクトで宣言する意味はない。(読み取り専用プロパティは別)
通常は下記のようにする。
・プロパティ宣言→コンストラクタにて
・メソッド宣言→プロトタイプにて

■静的プロパティ、静的メソッドの定義(インスタンス生成しなくてもオブジェクトから直接呼び出せるプロパティ、メソッド)
プロトタイプには登録できない。
コンストラクタ(オブジェクト)に直接追加する。
構文
オブジェクト名.プロパティ名 = 値
オブジェクト名.メソッド名 = function() {
メソッド定義
}

例(図形の面積を求める)

//コンストラクタ
var Area = function(){
}

//静的プロパティ定義
Area.version = '1.0';

//静的メソッドtriangle定義
Area.triangle = function(){
    return base * height / 2;
}

//静的メソッドdiamond定義
Area.diamond = function(width,height){
    return width * height / 2;
}

document.writeln('Areaクラスのバージョン' + Area.version); //1.0
document.writeln('三角形の面積', Area.triangle(5,3));//7.5
var a = new Area();
document.writeln();//エラー!

オブジェクトからversionなどの静的プロパティ、メソッドが呼びだせているが、
インスタンス経由で静的メソッドは呼びだせずにエラーとなる。
これは静的メンバがあくまでArea関数オブジェクトに動的追加されているが、
Areaが生成したインスタンスaには追加されてないため。

■静的プロパティ、メソッド定義の注意2点
1.静的プロパティは基本的に読み取り専用
2.静的メソッドの中ではthisキーワードが使えない

静的メンバを定義する理由はグローバル関数、変数を減らすため。
(クラス配下の関数、変数となるため予約語などとも競合しなくなる)

プロトタイプオブジェクトの不思議2(プロパティ削除)

上述した男、女というsexプロパティに戻り、今度はdelete mem1.sex、
delete mem2.sexとした場合、
mem1にはsexプロパティがないためなにもされず、
mem2.sexプロパティが削除される。
結果として男|男と表示されることになる。

つまり、インスタンス側でのメンバ追加削除などは元のプロトタイプオブジェクトにまで影響は及ぼさないということ。

オブジェクトリテラルでプロトタイプを定義する

ここまではドット演算子でプロトタイプにメンバを追加してきたが、
メンバ数が多い場合にコードが冗長化するので非推奨。。
上述のコードを例にすると下記のようにしたらよい。

var Member = function(firstName,lastName){
    this.firstName = firstName;
    this.lastName = lastName;
};

Member.prototype = {
    getName : function(){return this.lastName + ' ' + this.firstName; },
    toString : function(){return this.lastName + ' ' + this.firstName; }
};

オブジェクトの継承(プロトタイプチェーン)

元のオブジェクト(クラス)の機能を引き継いで新たなクラスを定義する機能。
重複がなくなり、差分プログラミングだけで済む。
継承元のクラスをスーパークラス
継承されたクラスをサブクラス
と呼ぶ。

プロトタイプチェーンの基礎

var Animal = function(){
}

Animal.prototype = {
    walk : function(){
        document.writeln('トコトコ....');
    }
}

var Dog = function(){
}
Dog.prototype = new Animal();

Dog.prototype.bark = function(){
    document.writeln('ワンワン!');
}

var d = new Dog();
d.walk(); //トコトコ...
d.bark();//ワンワン!

DogインスタンスからAnimalオブジェクト定義のwalkを呼び出せている。
(Dog自体にはwalkはない。)

継承関係の変更は動的に可能

JavaScript特有だが、動的に継承関係を変更できる。
ただし、動的とはいえども、インスタンスが生成された時点でプロトタイプチェーンは固定され、
その後の変更に関わらず保存されるので注意。

var Animal = function(){}
Animal.prototype = {
    walk : function(){document.writeln('トコトコ')}
}

var SuperAnimal = function(){}
SuperAnimal.prototype = {
    walk : function(){document.writeln('ダダダダ!')}
}

var Dog = funciton(){}
Dog.prototype = new Animal(); //Animalを継承
var d1 = new Dog();
d1.walk(); //トコトコ

Dog.prototype = new SuperAnimal(); //SuperAnimalを継承
var d2 = new Dog();
d2.walk(); //ダダダダ!
d1.walk(); //トコトコ

最後は変更されていてもAnimalの方のトコトコが表示される。


本格的な開発に備える

クラスライクな継承のしくみ

プロトタイプチェーンの継承は独特な概念であり、クラスベースのオブジェクト指向に慣れていると違和感がある。
そこでクラスベースによく似た継承方法を紹介する。

function initialzeBase(derive,base,baseArgs){
    base.apply(derive,baseArgs);
    for(prop in base.prototype) {
        var proto = derive.constructor.prototyepe;
        if(!proto[prop]){
            proto[prop] = base.prototype[prop];
        }
    }
}

//Memberクラス定義
var Member = function(firstName,lastName){
    this.firstName = firstName;
    this.lastName = lastName;
};

Member.prototype.getName = function() {
    return this.lastName + ' ' + this.firstName;
}

//Memberクラスを継承したSpecialMemberクラスを定義
var SpecialMember = function(firstName,lastName,role){
    initializeBase(this,Member, [firstName,lastName]);
    this.role = role;
}
SpecialMember.prototype.isAdministrator = function() {
    return (this.role == 'Administrator');
}

var mem = new SpecialMember('太郎' , '山田' , 'Administrator');
document.writeln('名前',+ mem.getName() );//山田 太郎
document.writeln('管理者' + mem.isAdministrator() );//

ここから先は一旦保留としておく。