JavaScript Diary

cloneメソッドの実装 [ 2001/10/14 ]
掲示板でも紹介しましたが、現在クロムレスウインドウなるものを作成しています。
この作成にあたり非常に困難な壁にぶち当たりました。それは

「オブジェクト指向で子ウインドウが開くようなものを作成し、親ウインドウは特に必要なく、子ウインドウに主な制御が移るようなものの場合、インスタンス作成元である親ウインドウが閉じられると機能しなくなる」

ということです。クライアントサイドJavaScriptでは「変数の寿命はそのウインドウの寿命とほぼ同じ」と考えて良いです。
つまり、ウインドウが閉じられるとそのウインドウに書かれた変数等は削除されてしまいます。

クロムレスの場合、親ウインドウで new Chromeless みたいな感じでインスタンスを作成、open メソッドでウインドウを開きます。そして、自分自身(インスタンス)を子ウインドウにコピーし、主な制御はこのコピーが行うという流れです。

さて、このコピーという言葉が怪しいですが、このコピーというのは勿論、親ウインドウにあるインスタンス(自分自身)をポインタで参照した変数を子ウインドウに用意するということです。

つまり、子ウインドウに存在するコピーは親ウインドウに存在するインスタンスへのポインタであり、結局親ウインドウが閉じられるとコピーも機能しなくなってしまいます。

そこでクロムレスクラスではこの不具合を解消するために

子ウインドウを開いた際に親ウインドウに存在するクロムレスクラス自体を丸々出力し複製。親ウインドウに存在するインスタンスを参考に、新しく子ウインドウ上に(子ウインドウ上のクロムレスクラスの)インスタンスを作成する。

という方法を取りました。実に面倒ですが、実際この方法しかないように思います。

前置きが長くなりましたが、このようにインスタンスに対して全く同じものを複製したい場合があります。
JavaScript にはありませんが、Java言語ではそれはcloneメソッドであり、各クラスでcloneメソッドを実装する仕組みが出来ています。
clone メソッドとは自分自身を複製するものです。例えば、
p = new Dimension( 0, 1 );
q = p.clone();
q は p への参照ではありません。完全に独立しています。

今回はこの clone メソッドを実装してみたいと思います。 しかも、クラス固有のものではなく、全てのクラスで使用できる汎用な複製メソッドを作成します。
いきなりですが、プログラムは以下のようになります。
Object.prototype.clone = function(){
    // オブジェクトの作成元クラス
    var ClassId = this.constructor ;
    // インスタンス生成
    var object = new ClassId ;
    // プロパティを全て抜き出す
    for( var prop in this ){
        // 作成元クラスの prototypeオブジェクト に存在する場合は無視
        if( this[ prop ] !== ClassId.prototype[ prop ] ){
            // オブジェクトの場合、再帰
            if( typeof this[ prop ] == "object" ){
                object[ prop ] = this[ prop ].clone();
            }else{
                object[ prop ] = this[ prop ];
            }
        }
    }
    return object ;
};
使用例は以下
var ary = [ 1, [ 2, 3 ], 4 ];
var copy = ary.clone(); // 1, 2, 3, 4
if( this[ prop ] !== ClassId.prototype[ prop ] ){ の部分は prototypeに含まれるメンバは複製する必要がないので加えている部分です。但し、(動的にインスタンスメソッドを上書きできるJavaScriptでは)用途によってはこの部分を削除した方が良い場合もあります。
このプログラムにはちょっとしたエラーがあるので、実際は以下のようになります。
Object.prototype.clone = function(){
    var ClassId = this.constructor ;
    var object = null ;
    if( ClassId === Boolean || ClassId === Number || ClassId === String ){
        object = new ClassId( this.valueOf() );
    }else{
        object = new ClassId ;
    }
    for( var prop in this ){
        if( this[ prop ] !== ClassId.prototype[ prop ] ){
            if( typeof this[ prop ] == "object" && this[ prop ].clone !== void 0 ){
                object[ prop ] = this[ prop ].clone();
            }else{
                object[ prop ] = this[ prop ];
            }
        }
    }
    return object ;
};
this[ prop ].clone !== void 0 を加えたのは window や document オブジェクト等には clone メソッドが含まれない(コンストラクタはObjectクラスのくせしてメソッド等は参照しない)からです。window オブジェクト等は複製することは出来ません。
ということはどっちにしろ本当の意味での clone メソッドは実装出来ないということになります。

話を戻しますが、このプログラムには怪しいところがあります。それは object = new ClassId ; です。
頭に入れておかなければならないことは「new している目的」とは、インスタンスを生成することではなく、コンストラクタ(型)を明示することと、もろもろのメソッド等を継承するためにあります。
組み込みオブジェクトが対象の場合、こう書いていても(結果的に)問題ないのですが、
ユーザ定義オブジェクトの場合、引数なしでインスタンスを生成(new)するとややこしくなる場合が多々あります。
生成する度に、ウインドウを開くクラス、出力するクラス、スタティックな変数を操作するクラス・・・、兎に角様々な不具合が生じるでしょう。
これを回避するには「引数なしの場合、何もしない」というクラス定義時の一貫したルールを作っておく必要がありますが、到底無理な話です。
ということで「ユーザ定義オブジェクトに関しては new させない方法」で clone メソッドを実装してみたいと思います。
Object.prototype.clone = function(){
    var ClassId = this.constructor ;
    var object = null ;
    if( ClassId === Boolean || ClassId === Number || ClassId === String ){
        object = new ClassId( this.valueOf() );
    }else if( ClassId === Array || ClassId === Function ){
        object = new ClassId ;
    }else{
        // Objectクラスからインスタンス生成
        object = new Object ;
        // コンストラクタを強制的に変更する
        object.constructor = ClassId ;
        // toString, valueOf メソッドは下記ループにて
        // 取得できないためここで prototypeオブジェクトから取得しておく
        object.toString = ClassId.prototype.toString ;
        object.valueOf = ClassId.prototype.valueOf ;
    }
    for( var prop in this ){
        if( typeof this[ prop ] == "object" && this[ prop ].clone !== void 0 ){
            object[ prop ] = this[ prop ].clone();
        }else{
            object[ prop ] = this[ prop ];
        }
    }
    return object ;
};
「なんだこの汚いプログラムは」とか「怪しい!怪しすぎるぞ!」っと思ったそこの君。実行してみたまえ。かなりな確率でうまくいきます。

但し、この方法の場合、↓のように書いてはいけないということと、それに対する副作用を考えておく必要があります。
for( var prop in this ){
    if( this[ prop ] !== ClassId.prototype[ prop ] ){
        if( typeof this[ prop ] == "object" && this[ prop ].clone !== void 0 ){
            object[ prop ] = this[ prop ].clone();
        }else{
            object[ prop ] = this[ prop ];
        }
    }
}
コンストラクタを強制的に変更しただけで、実際のコンストラクタはあくまでも Object であるということに注意が必要です。