JavaScript Diary

private インスタンス変数の実装2 [ 2001/09/26 ]
前回[9/25]、変数をランダム化することにより、現実的にアクセス不可能なインスタンス変数を作成できた。
しかし、汎用的にするため結果的にメソッドの内容が変更できなくなり実用的ではないものになってしまった。

今回はこれを解決するために、クラス固有でこのシステムをうまく利用することを考えたいと思います。
いきなりですが、結論は以下です。かなりトリッキーなものなので、よく見て理解してみて下さい。特にJava言語を知っている方は、形式がそれと近いので理解し易いかも知れません。
二次元の値を扱う Dimensionクラスで説明します。
// Dimension クラスを定義する関数
// 全てこの関数内で行う

function class__Dimension__(){
    
    // n桁のランダムな文字列(変数名)を作成する関数
    function createPrivateId( n ){
        var CODE_TABLE = "0123456789"
            + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            + "abcdefghijklmnopqrstuvwxyz";
        var r = "";
        for (var i = 0, k = CODE_TABLE.length; i < n; i++)
            r += CODE_TABLE.charAt(Math.floor(k * Math.random()));
        return r;
    }
    
    // ここが重要
    // 変数アクセスに必要な文字列を関数内で記憶しておく
    // インスタンス変数 x へアクセスするには this[$x] となる
    // この変数 $x, $y のスコープはこの関数(class__Dimension__)であるので、
    // グローバルスコープには含まれない(アクセス不可能)
    
    var $x = createPrivateId( 8 );
    var $y = createPrivateId( 8 );
    
    // コンストラクタ
    // ローカル内で普通に定義すると意味がないので、
    // スコープをグローバル(window)に変更する
    
    window.Dimension = function( x, y ){
        
        // 普通は this.x であるが、private 変数には
        // this[$x] という形でアクセスする
        
        this[$x] = x ;
        this[$y] = y ;
        
    }
    
    // 簡略化のため
    
    var F  = Dimension ;
    var FP = F.prototype ;
    
    // メソッド内でも同じように、アクセスには this[$x], this[$y] の形をとる
    
    FP.setX = function( x ){ this[$x] = x ; };
    FP.setY = function( y ){ this[$y] = y ; };
    
    FP.getX = function( x ){ return this[$x]; };
    FP.getY = function( y ){ return this[$y]; };
    
    FP.distance = function(){
        return Math.sqrt( Math.pow( this[$x], 2 )+Math.pow( this[$y], 2 ) );
    };
    
    FP.toString = function(){ return "x="+this[$x]+", y="+this[$y]; };
    
} class__Dimension__(); // 関数の呼び出し

var p = new Dimension( 1, 2 );

p.setX( 3 );
p.setY( 4 );

alert( "x="+p.getX()+", y="+p.getY()+", d="+p.distance() );

alert( p );

alert( p.x ); // undefined( x というプロパティは存在しない)
先ず、class__Dimension__ 関数ですが、この中で Dimensionクラス に関する全ての定義、実装を行います。今回のコツは関数内で定義することです。ローカル変数がグローバルスコープから隠蔽されることを利用します。
createPrivateId 関数は引数 n 桁のランダムな文字列を生成します。あとで説明しますが、この部分は他クラスでも同じロジックを使用するのでグローバルな位置で書いておいても結構です。この関数を使い private インスタンス変数の変数名を生成します。
次に var $x, var $y の部分ですが、これが非常に重要です。これに createPrivateId で生成した private変数名を文字列として保持させることになります。Dimensionクラス は x と y をプロパティとして保持しているように見せて、実際は8桁のランダムな変数名となります。$x$y のスコープは class__Dimension__ なので好きなように使用出来るというわけです。前回同様インスタンス変数名は毎回変更されます。
ここでインスタンス変数 x に対応する変数を this[$x] と設定することになるわけです。同様に y は this[$y] となります。もう一度書きますが、$x, $y はローカル変数です。p という Dimensionクラスのインスタンスを生成しても p[$x] は存在しませんし、p.$x とも出来ません。勿論、p.x も不可です。
次にコンストラクタ window.Dimension = function( x, y ){ ... の部分です。普通に function Dimension( x, y ){ ... と書いてしまうと、これはローカルでしか使用できないクラスになります(Java言語でいう内部クラスです)。ということで、これをグローバルで扱えるように window.Dimension = function( ... という形で宣言します。いつかの日記で書きましたが、window オブジェクトに追加されたプロパティはグローバルプロパティになります。ちなみに InternetExplorer では function window.Dimension( x, y ){ ... としても良いです。
あとは prototype に追加するメソッド内でのプロパティのアクセス方法が this.x ではなく this[$x] ということを注意するだけです。

余談になりますが、createPrivateId はこの書き方を定着すさせると全クラスで共通で使用できる関数となります。これをグローバルな関数にしますが、名前空間を汚さないように組み込みオブジェクトのクラスメンバに追加します。文字列を取り扱うので String クラスが良さそうですがクラスに深く関係するのでこれを区別し、Object に追加します。
Object.privateId = function(){
    var CODE_TABLE = "0123456789"
        + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        + "abcdefghijklmnopqrstuvwxyz";
    var r = "";
    for (var i = 0, k = CODE_TABLE.length; i < 8; i++)
        r += CODE_TABLE.charAt(Math.floor(k * Math.random()));
    return r;
}
故に var $x, var $y の部分は
var $x = Object.privateId();
var $y = Object.privateId();
となります。この方がよりエレガントだと思いますが、いかがでしょうか?

最後に Java言語と比べてみます。Java では以下のようになります。
class Dimension{
    // インスタンス変数の宣言
    private int x ;
    private int y ;
    // コンストラクタ
    public Dimension( int x, int y ){
        this.x = x ;
        this.y = y ;
    }
    // メソッドの宣言
    public void setX( int x ){ this.x = x ; }
    public void setY( int y ){ this.y = y ; }
    public int getX(){ return this.x ; }
    public int getY(){ return this.y ; }
    ...
}
JavaScript では
function class__Dimension__(){
    // private 変数名
    var $x = Object.privateId();
    var $y = Object.privateId();
    // コンストラクタ
    function window.Dimension( x, y ){
        this[$x] = x ;
        this[$y] = y ;
    }
    // メソッドの宣言
    Dimension.prototype.setX = function( x ){ this[$x] = x ; };
    Dimension.prototype.setY = function( y ){ this[$y] = y ; };
    Dimension.prototype.getX = function( x ){ return this[$x]; };
    Dimension.prototype.getY = function( y ){ return this[$y]; };
    ...
} class__Dimension__();
似てませんか?