JavaScript Diary

private インスタンス変数の実装(の真似) [ 2001/09/25 ]
動的なメンバ生成が可能である JavaScript にとってこのようなことを考えること自体無駄なのかも知れない。
9/15日(厳格な JavaScript に・・・)でも書いたように次世代の JavaScript の予約語には private なるキーワードが含まれる。
JavaScript1.0 時代、ArrayクラスをObjectクラスで代用していたように、現状の JavaScript で private なインスタンス変数をどうにか実現したいと思う。尚、2〜3回に分けて書いていく予定。

先ずは private について理解する必要がありますが、これはアクセスレベルを決めるキーワードの一つです。例えば以下のプログラムを見て下さい。
function C0( p0, p1 ){
    this.p0 = p0 ;
    this.p1 = p1 ;
}

// インスタンス生成
var c0 = new C0( 1, 2 );

// write
c0.p0 = 3 ;
// read
alert( c0.p0+", "+c0.p1 );
これは何の変哲もないプログラムです。JavaScript では↑のようにインスタンス変数への直接の書き込み(write)、読み込み(read)が許されています。また、これはあたり前のことです。
privateインスタンス変数とは、このようなインスタンスからの、またはグローバルな位置での write, read を許されないインスタンス変数のことを言います。
簡単なプログラムでは何の問題もありませんが、複雑なクラスの場合、この直接アクセスすることが思わぬバグを発生し、エラーを招くこともあります。
private の対語に public がありますが、JavaScript では全てのメンバは public であるといえます。JavaScript でも private, public を使い分けることにより必要なリソースだけをユーザに提供することは重要なことと考えます。

さて、今まで僕は private な変数と、public な変数とを区別するために private変数に関して変数名の先頭に $ を付けて表記していました。つまり以下のような感じになります。
function MyClass( p0, p1, p3 ){
    this.$p0 = p0 ; // private
    this.$p1 = p1 ; // private
    this.p3  = p3 ; // public
}

$ を付けた変数は外部からアクセスするべきではないと明示した自分流の書き方です。ユーザには $ の付いた変数はメソッドを介してアクセスして下さい、または全く変更する必要はありませんなど、その旨を書いておきます。
ですが、勿論これは気休めであり、普通のインスタンス変数と何ら変わりはありません。容易に直接アクセスでき、変更可能です。
そこで、先ずはこれらのprivate変数に関して、メソッドを介してアクセスするような方法を試みたいと思います。

function C1( p0, p1 ){
    this.p0( p0 );
    this.p1( p1 );
}
C1.prototype.p0 = function( p ){
    if( p === void 0 ){
        return this.$p0 ;
    }else{
        this.$p0 = p ;
    }
}
C1.prototype.p1 = function( p ){
    if( p === void 0 ){
        return this.$p1 ;
    }else{
        this.$p1 = p ;
    }
}

var c1 = new C1( 1, 2 );
c1.p0( 3 ); // write
alert( c1.p0()+", "+c1.p1() ); // read

このように private変数 $p0, $p1 に対して、メソッド p0, p1 を定義し、アクセスには全て対応するメソッドを使用するという仕組みです。
とりあえず、この形を作成しましたが、これでは private 変数が存在するだけメソッドを定義しなくてはいけず、これはかなり面倒な作業です。次にこれを自動で生成するような仕組みを考えます。
クラスの prototype オブジェクトにメソッドを自動で追加することを考えますが、ここで「全てのユーザ定義オブジェクト(クラス)は Object クラスを継承する」ことを思い出して下さい。
何やらこれでうまく行きそうですが、実際にはクラス自身(コンストラクタ関数)に関係するので Function オブジェクトを使用して以下のようになりました。
つまり、全てのコンストラクタ関数が make メソッドを保持することになります。

// private 変数にしたい変数名を引数にとり、
// それらに対するメソッドを自動生成する関数を prototype オブジェクトに追加
// 全てのコンストラクタ関数はこれを有する
Function.prototype.make = function(){
    for(var i=0;i<arguments.length;i++){
        // 以下で対象クラスの prototype オブジェクトに追加する
        this.prototype[ arguments[i] ] = function( prop ){
            return function( p ){
                if( p === void 0 ){
                    return this[ '$'+prop ];
                }else{
                    this[ '$'+prop ] = p ;
                }
            }
        }( arguments[i] );
    }
}

function C2( p0, p1 ){
    this.p0( p0 );
    this.p1( p1 );
}

// p0 と p1 は private 変数であり、ここでメソッドが追加される
C2.make( "p0", "p1" );

var c2 = new C2( 1, 2 );

c2.p0( 3 );
alert( c2.p0()+", "+c2.p1() );
分かり難いかも知れませんが、つまり C2.make( "p0", "p1" ) とした時点でインスタンス変数 $p0, $p1 に対するメソッドが生成されます。やっていることは先ほどの C1 クラスのものと変わりません。勿論、Function.prototype に追加された make メソッドは汎用ですので、このあと複数のクラスを定義しても同じように make メソッドを使用できます。

こんな変てこなプログラムを書きましたが、$p0 に直接アクセスはできるので何の解決にもなっていません。
ここで発想の転換、$p0 に対するメソッド p0 が定義されるわけですが、どうせ使わせたくないのなら $p0 を分からないものにしてしまおうというものです。
見た目、インスタンス変数の p0 が存在するかのように見せます。↑のプログラムの Function.prototype の部分に手を加えます。
Function.prototype.make = function(){
    for(var i=0;i<arguments.length;i++){
        this.prototype[ arguments[i] ] = function( id ){
            return function( p ){
                if( p === void 0 ){
                    return this[ id ];
                }else{
                    this[ id ] = p ;
                }
            }
        }( createRandomId( 8 ) );
    }
    // n 桁のランダムな文字列を生成する
    function createRandomId( n ){
        var res = "" ;
        for(var i=0;i<n;i++){
            switch( Math.floor( Math.random()*3 ) ){
                case 0 : res += String.fromCharCode( ffCode( 48, 10 ) ); break ;
                case 1 : res += String.fromCharCode( ffCode( 65, 26 ) ); break ;
                case 2 : res += String.fromCharCode( ffCode( 97, 26 ) ); break ;
            }
        }
        return res ;
        function ffCode( s, d ){ return Math.floor( s+d*Math.random() ); }
    }
}

function C3( p0, p1 ){
    this.p0( p0 );
    this.p1( p1 );
}

C3.make( "p0", "p1" );

var c3 = new C3( 1, 2 );

c3.p0( 3 );
alert( c3.p0()+", "+c3.p1() );
先ほどのプログラムでは、p0 に対する private変数名は一意に $p0 でしたが、このプログラムでは名前は決まりません。つまり、予測が不可能で、変数名が分からないため現実的にアクセス不可能な状態になります。
例えば、private変数 p0 は、実際には t8zN7fLo とかいうランダムな変数名として仮想的に存在するわけです。
また、変数名は make で指定した引数とは全く依存関係がなく推測は出来ません(この際、for..inループは考えません。笑)。
この場合、変数名は8桁になっているので、その組み合わせ総数は 62^8 と天文学的数字です。走査することも現実的ではありませんし、バッティングもありえません。
また、make を呼び出す(ページをリロードする)たびにインスタンス変数名が変わることも面白いことかと思います(これに関する副作用のエラーはありません)。

さて、privateインスタンス変数は一応実現できました。
しかし、直接アクセスできないというだけで、Function.prototype の内容でメソッドの処理が決まってしまうため、実用的にはほど遠いものがあります。
実用的にしようと考えるとやはり、そのクラス独自の処理がメソッドに求められ、クラス固有でこのような仕組みを考える必要があります。
次回はこれとはまた別の方法で privateインスタンス変数を作成したいと思います。出来る出来ないは別として・・・

# 関係ないけど、このページも重くなってきたねぇ〜。いつか分けます。