ユーザ定義オブジェクト

はじめに
ここでは JavaScript の高度な書き方。JavaScript のユーザ定義オブジェクトの作成方法について書きたいと思います。
JavaScript は オブジェクト指向型言語 と呼ばれ、本格的なオブジェクト指向ではありませんが、オブジェクト指向言語の Java(TM) などに近いことはできるようです。
尚、オブジェクトなどと書いていますが、厳密にはこれは違う意味で使われるので、ここではクラスと呼ぶことにします。このサイトでもこれは統一してこの言葉を使うことにします。
オブジェクトって何?
僕から言及すると実に危ういので(^^);、ある書籍から一部引用。
オブジェクトとはその名の通り「物」のことです。現実世界には色々な物があります。例えば、はさみ、鉛筆、本などは「物」です。
そういった物には「状態」や「ふるまい」があります。例えば、鉛筆であれば、「状態」は線の色、色の濃さ、線の太さなどで、「ふるまい」は線が引けるなどです。
こういった物の「状態」や「ふるまい」をプログラムに持ち込んだものがオブジェクト指向です。
実際にはプログラムでは「状態」を変数で表し、「ふるまい」を関数(メソッド)で表すことになります。
そして、これらの変数や関数のことを JavaScript では、プロパティ、メソッドと呼びます。
つまり、JavaScript でいうオブジェクトというのは、「プロパティとメソッドを詰め込んだ箱」と理解して頂いても結構です。
クラスって何?
オブジェクトとクラスは意味が違います。ここでも僕から言及するのは実に危なっかしいのですが、
例えば「手に持っている鉛筆」は物(オブジェクト)ですが、「鉛筆と考えられている物」はオブジェクトではなく、「クラス」ということになります。
つまり、クラスというのは概念的な雛型であり、それから実際に生成される物がオブジェクトということになります。
プログラムでは、クラスはそういった「状態」や「ふるまい」を定義した箱で、オブジェクトは実際に動作するものです。
そして、僕はそのクラスから生成されたものを Java(TM) に習い、インスタンス と呼ぶことにします。
例えば、JavaScript には日付に関するオブジェクト Date オブジェクトがありますが、Date はクラスで
var date = new Date();
の date は Date クラスのインスタンスということになります。
何やら難しいですね。これらを別に知っておかなくてもプログラムは書けちゃいますので、オブジェクト指向については他書籍等を参考にして下さい。
オブジェクト指向(クラス化)の利点
プログラムにはさまざまな部品があり、それらが個々の機能を持つことでプログラム全体としての機能を果たすことになります。
クラス化することにより機能が一まとめにされますので、独立性が高まり、何と言っても扱い易いものになるでしょう。
ネット上においては、更新が容易になり、他人の作ったプログラムをそのまま拡張して使用することも出来るようになります。
近い将来、僕は JavaScript はそういう言語になるものと信じております。
プログラムを書いてみる
では、実際にプログラムを書いてみましょう。
クラス化にはオブジェクトリテラルを使った方法と コンストラクタ関数 を使った方法とがありますが、ここでは応用の効くコンストラクタ関数を使ったもので説明します。
コンストラクタ関数といっても、別に JavaScript に特別な構文があるわけでもなく、普通の関数と同じように書くことが出来ます。
拡張性を考えて、「長方形」を表すクラス、Rectangle クラスを作成することにします。
長方形を定義するために必要な情報は 幅(width) と 高さ(height) です。
function Rectangle( w, h ){
    this.width  = w ; // 長方形の幅
    this.height = h ; // 長方形の高さ
}
これで Rectangle クラスのコンストラクタ関数は定義できました。
とんでもなく、あっけないです(笑)
Rectangle クラスは長方形の幅と高さを表す、width と height をプロパティとして保持しています。
this キーワードでプロパティを決定していることに注意して下さい。また、コンストラクタはプロパティを初期化するだけで、return 文を書く必要はありません。
では、実際に Rectangle クラスのインスタンスを生成してみましょう。
var r0 = new Rectangle( 2, 1 );
var r1 = new Rectangle( 4, 2 );
これで r0 と r1 は Rectangle クラスのインスタンス(オブジェクト)になりました。
このようにインスタンスを生成するには new 演算子を使うことになります。
これからプロパティ width,height を取り出すには次のようにします。
status = r0.width ; // 結果は 2
ドット(.)演算子 を使用して、その後ろにプロパティ名を指定することになります。簡単ですね。
メソッドを定義する
インスタンスを介して呼び出す関数のことをメソッドと呼びます。
追加するメソッドは長方形の面積を取得するものにしましょう(面積=幅×高さ)
先ほどのプログラムをそのまま引用して書いてみます。
// コンストラクタ関数の定義
function Rectangle( w, h ){
    this.width  = w ; // 長方形の幅
    this.height = h ; // 長方形の高さ
}

// この関数がメソッドになる。
// 内部で this キーワードを使用していることに注意
function getArea_Rectangle(){
    return this.width * this.height ;
}

// インスタンス生成
var r0 = new Rectangle( 3, 2 );

// メソッドの定義
r0.getArea = getArea_Rectangle ;

// メソッドの呼び出し
status = r0.getArea(); // 結果は 3*2 で 6
このようにメソッドを追加するときはドット(.)演算子の後ろでメソッド名を指定して = のあとに実際に処理する関数を指定します。簡単ですよね。
ですが、これには問題点があります。それは、このメソッドを使えるのが r0 のみであることです。
例えば、このあと、新しく r1 というインスタンスを生成しても、r1.getArea() とは書けません。
普通はこういったメソッドは共有させ、全インスタンスで使用したいものです。
次項ではその共有方法について説明します。
メソッドの共有
上で説明したメソッドは r0 のみしか使用できないものでした。
では、メソッドやらを全インスタンスで共有して使用するにはどうすれば良いのでしょうか?
但し、width や height といったインスタンス固有のものではなく、メソッドなどの機能だけを共有したいということです。
それには、prototype プロパティ(オブジェクト)を使用します(prototype プロパティについての説明については、かなり長くなりますのでここでは説明しません。というか僕もよく分かっていないので、説明できないだけです)。
とりあえず、プログラムを見てみましょう。
// コンストラクタ関数の定義
function Rectangle( w, h ){
    this.width  = w ; // 長方形の幅
    this.height = h ; // 長方形の高さ
}

// この関数がメソッドになる。
// 内部で this キーワードを使用していることに注意
function getArea_Rectangle(){
    return this.width * this.height ;
}

// メソッドの定義
Rectangle.prototype.getArea = getArea_Rectangle ;

// インスタンス生成
var r0 = new Rectangle( 3, 2 );
var r1 = new Rectangle( 4, 6 );

// メソッドの呼び出し
status = r0.getArea(); // 結果は 3*2 で  6
status = r1.getArea(); // 結果は 4*6 で 24
prototype で追加されたメソッドは全インスタンスで共有(に継承)されます。
これで、ユーザ定義オブジェクトの基本は終了です。簡単でしたよね。

次項ではクラス化したときの注意点について書いています。
よくある間違い
クラス化して最初に間違え、悩むことは恐らくココに書いていることです。
重要なので必ず、読んでください。status バーに何が表示されるか考えながら読んでみて下さい。
例えば以下のプログラムです。
var r0 = new Rectangle( 3, 2 );
var r1 = new Rectangle( 4, 6 );

status = ( r0 == r1 );
これは、予想通り false が表示されます。では、次はどうでしょう。
var r0 = new Rectangle( 1, 1 );
var r1 = new Rectangle( 1, 1 );

status = ( r0 == r1 );
true が返されると思いますか?
それは間違いです。やっぱり、false が返されます(中身が同じでも参照している場所が違うのでこういう結果になります)。 では、次はどうだと思いますか?
var r0 = new Rectangle( 1, 1 );
var r1 = r0 ;

status = ( r0 == r1 );
これは true が表示されます(これは参照している場所が一緒のためにこうなります)。
これらはポインタと呼ばれる概念がそういう結果を生みます(ここでは説明しません)。

これと関連して次のプログラムを見てみましょう。
var r0 = new Rectangle( 4, 3 );
var r1 = r0 ;

r0.width = 5 ;

status = r1.width ;
答えは 4 ではなく、5 です。
つまり、r1 = r0 とした時点で r0 と r1 は同じところを参照し、このままではずっと連動してふるまうことになります。
意図通りに表示したい(4を表示したい)場合は次のように書きます。
var r0 = new Rectangle( 4, 3 );
var r1 = new Rectangle( r0.width, r0.height );

r0.width = 5 ;

status = r1.width ;
こうしておけば、ステータスバーに表示されるのは 4 です。
これらが基本型(number,boolean,string)と参照型(object,array,function)の大きな違いですが、分かりましたでしょうか?
最初は戸惑うかもしれませんが、慣れると気にならなくなります。

次項からは、これだけでは物足りないという人にもう一歩進んだオブジェクト指向について説明します。
言葉の定義
1歩進む前に言葉の定義をはっきりしておきましょう。
JavaScript は本格的なオブジェクト指向言語でないためか、結構曖昧な言葉が多いように思われます。何を隠そう。僕もそのなかの一人です。
恐縮ながら、Java(TM) で学んだ僕なりの定義を示しておきます。

クラス:物の状態やふるまいを定義した箱(Rectangleなど)
インスタンス:クラスから生成(new)された実際のもの(r0,r1など)
インスタンス変数:インスタンス固有で持っている値(width,height など)
インスタンスメソッド:クラスのインスタンス全てで共有する関数(getArea など)
クラス変数:クラスそのものに関連付けられた変数(Math.PI など)
クラスメソッド:クラスそのものに関連付けられた関数(String.fromCharCode など)
メンバ:クラスの内部にある変数などの構成要素全体

何もかもプロパティと呼んでいましたがこれだけでも、きちんとしたはずです。
次項からはこれらについて説明しますが、インスタンス、インスタンス変数、インスタンスメソッドについてはもう理解できましたよね。
クラス変数、クラスメソッド
JavaScript でクラス変数、クラスメソッドを表現してみましょう。
これも操作的には実に簡単です。例は Rectangle クラスに√2の値を追加することにします。
Rectangle.SQRT2 = 1.414 ;
終わりです(笑)
つまり、クラス変数とは、クラスそのものに関連付けされたものですから、その名の通りクラスに変数を追加します。
次はクラスメソッドですが、もう分かりましたね(笑)
例では二つの Rectangle クラスのインスタンスの面積を調べ、大きい方の値を返すメソッドを追加することにします。
function Rectangle( w, h ){
    this.width  = w ; // 長方形の幅
    this.height = h ; // 長方形の高さ
}

function getArea_Rectangle(){
    return this.width * this.height ;
}

// インスタンスメソッドの定義
Rectangle.prototype.getArea = getArea_Rectangle ;

// この関数が Rectangle クラスのクラスメソッドになる
function getAreaMax_Static_Rectangle( p, q ){
    return Math.max( p.getArea(), q.getArea() );
}

// クラスメソッドの定義
Rectangle.getAreaMax = getAreaMax_Static_Rectangle ;

// インスタンス生成
var r0 = new Rectangle( 3, 2 );
var r1 = new Rectangle( 4, 6 );

status = Rectangle.getAreaMax( r0, r1 ); // 結果は 24
同じようにクラス名のあとにメソッド名を書けばよいです。
クラスメンバの作成は全インスタンスを統括して扱うなど、これを利用すると実に設計性の高いすっきりした綺麗なプログラムを作成できるようになります。
クラスの継承
JavaScript の現状ではあまり使うことはないかも知れませんが、クラスを継承するときは以下のようにします。
function SuperClass(){
    ...
}

function SubClass(){
    ...
}

SubClass.prototype = new SuperClass ;
prototype オブジェクトって便利ですね(笑)
new SuperClass の後ろに括弧がないのは new SuperClass() と書いたのと同じで、new 演算子の特例です。引数がないときにこのように省略できます。
尚、サブクラスに親クラスのプロパティを使用して、初期化したい場合は次のようにすると良いです。
function SuperClass( p, q ){
    this.p = p ;
    this.q = q ;
}

function SubClass( p, q ){
    this.constructor( p, q );
}

SubClass.prototype = new SuperClass ;
constructor 関数を使用すればこのように簡潔にかけます(Javaのsuperみたいに使用してます)。 constructor 関数は作成元の関数を返すので、これを見ると prototype プロパティの働きが少しは分かるのではないでしょうか。

継承に関してですが、恐らく将来はこのような方法ではなく、正規に extends という予約語を使って表現されるものと予想されます。
toString, valueOf メソッド
これら二つのメソッドは実に特殊な振る舞いをします。
先ず、toString メソッドから説明しましょう。
例には Rectangle クラスを使用します。
// これが Rectangle クラスの toString メソッドになる
function toString_Rectangle(){
    return "長方形 : "+this.width+"×"+this.height ;
}

function Rectangle( w, h ){
    this.width  = w ;
    this.height = h ;
}

Rectangle.prototype.toString = toString_Rectangle ;

var r0 = new Rectangle( 3, 4 );

// 表示
status = r0 ; // 結果は????
これを実行すると何がステータスバーに表示されると思いますか?
答えは "object" でも、"undefined" でもなく
"長方形 : 3×4"
です。つまり、toString メソッドはそのクラスのインスタンスが文字列型で参照されたときに呼び出され実行されます。
これがデバッグに役立つことは言うまでもありません。
クラスを作成したときは、toString メソッドを定義しておくことを勧めます。

次に valueOf メソッドですが、toString が文字列型であったのに対し、valueOf メソッドは文字列型以外の基本型(主に数値型)で参照されたときに呼び出されます。
ここでは、あまり実用的ではないですが、面積を返すようにします。
function valueOf_Rectangle(){
    return this.width*this.height ;
}

function Rectangle( w, h ){
    this.width  = w ;
    this.height = h ;
}

Rectangle.prototype.valueOf = valueOf_Rectangle ;

var r0 = new Rectangle( 3, 4 );

status = r0-0 ; // 結果は 3*4-0 で 12
-0 を付加しているのはこれが数であることを明示的にするためです。toString メソッドと valueOf メソッドのどちらが呼び出されるかは用途によって違います。toSring メソッドを呼び出したいときは r0+"", valueOf メソッドを呼び出したいときは r0-0 などのようにするか、r0.toString() などのように明示的に記すしか方法はありません。
さいごに
さいごに恐縮ですが、僕なりのクラス化の方法と記述の仕方について書きたいと思います。
僕が一番気にするのは、バッティング(名前の競合)です。
その点ではクラス化は最高の方法でした。
以下には Rectangle クラスを僕なりに書き換えたものを記述しておきます。
インスタンスメソッドには、
値を変更する set,setWidth,setHeight
値を取得する getWidth,getHeight
引数とのインスタンス値が等しいか調べる equals
面積を返す getArea
対角線の長さを返す getDigo
width と height を比べて大きい方の値を返す max、小さい方を返す min
width と height をスケーリングする scale
width と height を半分の値にする half
width と height を配列に格納したものを取得する toArray
width と height が等しいとき、true を返す valueOf
標準出力文字列 toString
を定義し、クラスメソッドには、
任意個数の Rectangle クラスを引数にとり、面積の平均値を返す getAveArea
を定義しています。
// コンストラクタの定義
// 
// ----- 宣言の形式 -----
// 
// new Rectangle()
// width = height = 1.0 ;
// 
// new Rectangle( Rectangle r )
// width = r.width, height = r.height ;
// 
// new Rectangle( number n ) : おまけ
// width = height = n ;
// 
// new Rectangle( number w, number h )
// width = w, height = h ;
function Rectangle(){
    
    var a = arguments ; // 引数
    
    // 引数の個数により、処理を分岐
    switch( a.length ){
        
        // 引数が0個のとき
        case 0 : default : this.width = this.height = 1.0 ; break ;
        
        // 引数が1つのとき
        case 1 : {
            // 引数の型が Rectangle
            if( a[0].constructor == Rectangle ){
                this.width  = a[0].width ;
                this.height = a[0].height ;
            }else{
                this.width = this.height = a[0];
            }
            break ;
        }
        
        // 引数が2つのとき
        case 2 : {
            this.width  = a[0];
            this.height = a[1];
            break ;
        }
    }
}

// 関数内でメソッドを定義することにより、限りなくバッティングを減らすことが可能になる。
// この関数内でさらに関数を定義できるということにも注意。
// 今回、全てのメソッド定義には無名関数(関数リテラル)を使用している。
function setBasicMember__Rectangle__(){
    
    var R  = Rectangle ;
    var RP = R.prototype ; // メソッドが多い場合に便利
    
    /************* クラスメソッド *************/
    
    R.getAveArea = function(){
        var s = 0 ;
        var a = arguments ;
        for(var i=0;i<a.length;i++) s += a[i].getArea();
        return s/a.length ;
    };
    
    /********** インスタンスメソッド **********/
    
    RP.set = function(){
        var a = arguments ;
        var r ;
        switch( a.length ){
            case 0 : default : r = new Rectangle(); break ;
            case 1 : r = new Rectangle( a[0] ); break ;
            case 2 : r = new Rectangle( a[0], a[1] ); break ;
        }
        // 新しく生成し、そのインスタンスから値を参照する。
        // このテクニックはファイルサイズを減らしたいとき有効
        this.width  = r.width ;
        this.height = r.height ;
    };
    
    // 以下のようにメソッドに複数の名前を与えることも可能
    
    RP.setWidth  = RP.setW = function( w ){ this.width = ( w ? w : 1.0 ); };
    RP.setHeight = RP.setH = function( h ){ this.height = ( h ? h : 1.0 ); };
    
    RP.getWidth  = RP.getW = function(){ return this.width ; };
    RP.getHeight = RP.getH = function(){ return this.height ; };
    
    // このようなクラスの場合、equals メソッドを定義しておくのが定石
    RP.equals = function( r ){
        return ( ( this.width == r.width ) && ( this.height == r.height ) );
    };
    
    RP.getArea = function(){ return this.width * this.height ; };
    
    // 対角線の長さを求める式は √( width^2 + height^2 )
    RP.getDigo = function(){
        return Math.sqrt( Math.pow( this.width, 2 )+Math.pow( this.height, 2 ) );
    };
    
    RP.max = function(){ return Math.max( this.width, this.height ); };
    
    RP.min = function(){ return Math.min( this.width, this.height ); };
    
    RP.scale = function( v ){
        this.width  *= v ;
        this.height *= v ;
    };
    
    // 他メソッドをメソッド内で使用することも勿論、可能
    RP.half = function(){ this.scale( 0.5 ); };
    
    RP.toArray = function(){ return new Array( this.width, this.height ); };
    
    RP.valueOf = function(){ return ( this.width == this.height ); };
    
    RP.toString = function(){ return "Rectangle : "+this.width+" × "+this.height ; };
    
} setBasicMember__Rectangle__(); // 関数の呼び出し
ユーザ定義オブジェクトの作成例については Class Library のページもあるので、そちらも参考にして下さい。