API Only - Stack Exchangeに詳しい解説があったので翻訳する。
質問
オブジェクトxをオブジェクトyにコピーしたい。yを変更してもxが変更されないようにだ。JavaScriptで、最もエレガントな方法は?
追記: JavaScriptの組み込みオブジェクトをコピーすると不要なプロパティをコピーしてしまうことは理解している。今回は問題にならない。リテラルで生成した自前のオブジェクトを対象にするからだ。
回答
JavaScriptには、あらゆるオブジェクトをコピーできるシンプルで統一的な方法ありません。まず、リンクしたプロトタイプオブジェクトから属性を取得してしまう問題があります。プロトタイプオブジェクトの属性は、新しいインスタンスにコピーすべきはありません。Object.prototypeにcloneメソッドを追加するのであれば、プロトタイプオブジェクトの属性を明示的に除外する必要があります。また、Object.prototypeや他中間のプロトタイプオブジェクトに未知メソッドが追加されているかもしれません。この場合もこれら属性をコピーすべきではありません。これらの属性を除外するにはhasOwnPropertyメソッドで予期しない、ローカルオブジェクト以外の属性を検出する必要があります。
さらにfor in文で列挙できない属性が存在します。オブジェクトが非表示プロパティを持つことがあります。たとえばprototypeはFunctionオブジェクトの非表示プロパティです。また、オブジェクトのprototypeのうち__proto__属性で参照されるものも非表示です。ソースオブジェクトの属性をfor in文で列挙した場合、無視されます。__proto__はFirefoxのJavaScriptエンジン固有のもので、他のブラウザでは無効かもしれません。ともあれ、すべての属性が列挙可能ではないことはわかってもらえると思います。名前を知ってさえいれば非表示プロパティをコピーすることはできますが、自動的に非表示プロパティを検出する方法はありません。
またエレガントな解法を得るには別の暗礁もあります。prototype継承を正しく設定する問題です。ソースオブジェジェクトのprototypeがObjectであれば{}を使って新しいオブジェクトを生成すればよい。そうではなくソースオブジェクトがObjectの子孫である場合には、hasOwnPropertyでフィルターしているためプロトタイプから追加されたメンバを見つけることはできない。また、プロトタイプにあるメンバが列挙できない場合もある。
一つの解法はコピーオブジェクトの初期化の際にソースオブジェクトのconstructor プロパティを呼び出して属性をコピーすることだ。しかしそれでも列挙できない属性がある。次の例のようにDateオブジェクトはデータを隠しメンバーとして持っている。
function clone(obj) { if (null == obj || "object" != typeof obj) return obj; var copy = obj.constructor(); for (var attr in obj) { if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr]; } return copy; } var d1 = new Date(); /* Wait for 5 seconds. */ var start = (new Date()).getTime(); while (new Date()).getTime() - start < 5000); var d2 = clone(d1); alert("d1 = " + d1.toString() + "\nd2 = " + d2.toString());
上の関数は、オブジェクトと配列のデータがツリー構造であれば、私が言及した6つの単純な型に対して十分に機能します。つまり、同じデータへの複数の参照が無ければです。たとえば、
// これはcloneできます: var tree = { "left" : { "left" : null, "right" : null, "data" : 3 }, "right" : null, "data" : 8 }; // これはすこし働きます。 // しかし、同じコピーへの2つの参照の代わりに2つの内部ノードを得るでしょうvar directedAcylicGraph = { "left" : { "left" : null, "right" : null, "data" : 3 }, "data" : 8 }; directedAcyclicGraph["right"] = directedAcyclicGraph["left"]; // これをcloneすると無限再帰によるスタックオーバーフロー起こします。 var cylicGraph = { "left" : { "left" : null, "right" : null, "data" : 3 }, "data" : 8 }; cylicGraph["right"] = cylicGraph;
あらゆるJavaScriptオブジェクトを扱うことはできません。しかし全ての目的のためにでも動くことを求めなければ大抵の目的には十分です。