「プロパティのコピー パターン」と呼ばれるコード再利用パターンの一種があります。コードの再利用について考えるとき、コードの継承を思い浮かべる可能性が高いですが、最終的な目標はコードを再利用することであることを覚えておくことが重要です。継承はコードの再利用を実現するための単なる手段であり、唯一の方法ではありません。プロパティのコピーも再利用パターンであり、継承とは異なります。このパターンでは、オブジェクトは単にコピーするだけで別のオブジェクトからメンバーを取得します。 jQuery を使用したことのある人は、サードパーティのプラグインを拡張するだけでなく、属性をコピーするためにも使用できる $.extend() メソッドがあることを知っています。 extend() 関数の実装コードを見てみましょう (これは jQuery のソース コードではなく、単なる単純な例であることに注意してください):
function extend(parent, child) { var i; //如果不传入第二参数child //那么就创建一个新的对象 child = child || {}; //遍历parent对象的所有属性 //并且过滤原型上的属性 //然后将自身属性复制到child对象上 for(i in parent) { if(parent.hasOwnProperty(i)) { child[i] = parent[i]; } } //返回目标对象child return child; }
上記のコードは単純な実装であり、親オブジェクトのメンバーを走査し、子オブジェクトにコピーするだけです。上記の extend() メソッドを使用してテストしてみましょう:
var dad = {name: "Adam"}; var kid = extend(dad); console.log(kid.name); //Adam
extend() メソッドがすでに正常に動作していることがわかりました。しかし、上に挙げたものはいわゆるシャロー クローンです。浅いコピーを使用する場合、子オブジェクトのプロパティを変更し、そのプロパティがオブジェクトである場合、この操作により親オブジェクトも変更されることになり、多くの場合、これは望ましい結果ではありません。次の点を考慮してください:
var dad = { counts: [1, 2, 3], reads: {paper: true} }; var kid = extend(dad) //调用extend()方法将dad的属性复制到kid上面 kid.counts.push(4); //把4追加到kid.counts数组里面 console.log(dad.counts); //[1, 2, 3, 4]
上記の例を通して、kid.counts 属性を変更した後 (要素 4 を追加)、dad.counts も影響を受けることがわかります。これは、浅いコピーを使用する場合、オブジェクトが参照によって渡されるため、つまり、kid.counts とdad.counts が同じ配列を指す (またはメモリ内で、同じヒープ アドレスを指す) ためです。
次に、extend() 関数を変更してディープコピーを実装しましょう。私たちがしなければならないことは、親オブジェクトの各属性をチェックし、その属性がオブジェクトである場合は、そのオブジェクトの属性を再帰的にコピーすることです。さらに、オブジェクトが配列であるかどうかも検出する必要があります。これは、配列のリテラルの作成方法がオブジェクトのリテラルの作成方法と異なるためです。前者は [] で、後者は {} です。配列を検出するには、Object.prototype.toString() メソッドを使用できます。配列の場合は、「[object Array]」を返します。ディープコピーバージョンの extend() 関数を見てみましょう:
function extendDeep(parent, child) { child = child || {}; for(var i in parent) { if(parent.hasOwnProperty(i)) { //检测当前属性是否为对象 if(typeof parent[i] === "object") { //如果当前属性为对象,还要检测它是否为数组 //这是因为数组的字面量表示和对象的字面量表示不同 //前者是[],而后者是{} child[i] = (Object.prototype.toString.call(parent[i]) === "[object Array]") ? [] : {}; //递归调用extend extendDeep(parent[i], child[i]); } else { child[i] = parent[i]; } } } return child; }
ディープ コピー関数が作成されました。期待どおりに機能するかどうか、つまりディープ コピーが実現できるかどうかをテストしてみましょう。
var dad = { counts: [1, 2, 3], reads: {paper: true} }; var kid = extendDeep(dad); //修改kid的counts属性和reads属性 kid.counts.push(4); kid.reads.paper = false; console.log(kid.counts); //[1, 2, 3, 4] console.log(kid.reads.paper); //false console.log(dad.counts); //[1, 2, 3] console.log(dad.reads.paper); //true
上記の例から、子オブジェクトの kid.counts と kid.reads が変更されても、親オブジェクトのdad.counts と kid.reads は変更されていないことがわかり、目的は達成されています。
ディープ コピーを実装するための基本的な考え方の概要を以下に示します。
1. 現在の属性がオブジェクトであるかどうかを確認します
2. 配列は特殊なオブジェクトであるため、属性がオブジェクトの場合は配列であるかどうかを検出する必要があります。
3. 配列の場合は、[] 空の配列を作成し、そうでない場合は、{} の空のオブジェクトを作成し、それを子オブジェクトの現在のプロパティに割り当てます。次に、extendDeep 関数が再帰的に呼び出されます。
上記の例では、再帰アルゴリズムを使用して独自に実装されたディープ コピー メソッドを使用しています。実際、ES5 の新しい JSON オブジェクトによって提供される 2 つのメソッド、つまり JSON.stringify() と JSON.parse() もディープ コピーを実現できます。前者はオブジェクトを文字列に変換するために使用され、後者はオブジェクトを文字列に変換するために使用されます。文字列をオブジェクトに変換します。以下では、このメソッドを使用してディープ コピー機能を実装します。
function extendDeep(parent, child) { var i, proxy; proxy = JSON.stringify(parent); //把parent对象转换成字符串 proxy = JSON.parse(proxy) //把字符串转换成对象,这是parent的一个副本 child = child || {}; for(i in proxy) { if(proxy.hasOwnProperty(i)) { child[i] = proxy[i]; } } proxy = null; //因为proxy是中间对象,可以将它回收掉 return child; }
以下はテスト例です:
var dad = { counts: [1, 2, 3], reads: {paper: true} }; var kid = extendDeep(dad); //修改kid的counts属性和reads属性 kid.counts.push(4); kid.reads.paper = false; console.log(kid.counts); //[1, 2, 3, 4] console.log(kid.reads.paper); //false console.log(dad.counts); //[1, 2, 3] console.log(dad.reads.paper); //true
テストの結果、ディープコピーも実現できることがわかりました。 JSON.parse と JSON.stringify は組み込み関数であり、より高速に処理されるため、通常は後者の方法を使用することをお勧めします。さらに、前の方法では再帰呼び出しが使用されており、再帰が比較的非効率なアルゴリズムであることは誰もが知っています。
JavaScript ディープクローンの実装方法については以上です。お役に立てれば幸いです。