ディープコピーとシャローコピー:
始める前に、まず浅いコピーと深いコピーが何であるかを理解する必要があります。実際、深いコピーと浅いコピーは両方とも参照型です。JS の変数の型は、値型 (基本型) と参照型に分けられます。値型については、コピー操作では値のコピーが作成され、参照型の代入ではアドレスのコピーが作成され、最終的に 2 つの変数は同じデータを指します。
// 基本类型
var a = 1;
var b = a;
a = 2;
console.log(a, b); // 2, 1 ,a b指向不同的数据
// 引用类型指向同一份数据
var a = {c: 1};
var b = a;
a.c = 2;
console.log(a.c, b.c); // 2, 2 全是2,a b指向同一份数据
参照型の場合、ab は同じデータを指します。このとき、一方が変更されると、もう一方にも影響します。場合によっては、これが望んでいる結果にならない場合があります。この現象についてよくわからない場合は、次のことが考えられます。不要なバグを引き起こす。
では、a と b の関係を断ち切るにはどうすればよいでしょうか? a のデータをコピーできます。コピーのレベルに応じて、浅いコピーと深いコピーに分けることができます。浅いコピーとは 1 層のコピーのみを意味し、深いコピーとは、1 層のコピーのみを意味します。コピーとは無限レベルのコピーを意味します。
var a1 = {b: {c: {}}};
var a2 = shallowClone(a1); // 浅拷贝
a2.b.c === a1.b.c // true
var a3 = clone(a1); // 深拷贝
a3.b.c === a1.b.c // false
ここで、shallowClone は浅いコピー、clone は深いコピーを指し、a1 オブジェクトは 3 層のネストされたオブジェクトであると考えられます。浅いコピーは最初のレイヤーのみをコピーできます。b 属性はオブジェクトです。このオブジェクトの参照アドレスは b に格納されているため、a1.bc===a2.bc; 深いコピーは各レイヤー、a1.b と参照をコピーします。 a3.b のアドレスは異なり、完全に独立しているため、a3.bc!=a1.bc
浅いコピーの実装は非常に簡単です。コピーされるオブジェクトまたは配列をトラバースし、作成された新しいオブジェクトまたは配列に割り当てて、浅いコピーを完了します。ここでは詳細は説明しません。以下では、ディープコピーをどのように実装すべきかについて説明します。ここではディープコピーの方法を4つ紹介します。
1. 再帰的方法:
ディープ コピーを実装するために最も一般的に使用される方法は再帰的方法であり、その原理は、コピーされるオブジェクトまたは配列を再帰的に走査することです。まず、コピーするオブジェクトまたは配列の型を判断し、対応する空のオブジェクトまたは配列を作成し、シャロー コピーと同様にオブジェクトまたは配列をループで反復処理します。 type, recurse ディープ コピー関数を呼び出します。属性が基本データ型の場合、すべてのサブオブジェクトのすべての属性がスキャンされるまで、代入操作が直接実行されます。再帰的トラバーサルは本質的に深さ優先トラバーサルです。ディープ コピーを実行するとき、属性の値がオブジェクトの場合、この属性に隣接する次の属性をコピーするのではなく、サブオブジェクトが最初にコピーされます。次の属性は、この属性に対応するサブオブジェクト全体がコピーされた後にのみコピーされます。再帰メソッドのコードは次のとおりです。
//递归遍历每个属性进行深拷贝
function clone(source) {
let target = Array.isArray(source) ? [] : {};
if(typeof(source)!="object") {
target = source;
return target;
}
if(source instanceof Array) {
for(var i=0;i<source.length;i++) {
if(typeof source[i] == "object") {
target[i] = clone(source[i]);
}else {
target[i] = source[i];
}
}
}
else {
for(let key in source) {
if(source.hasOwnProperty(key)) {
if(typeof source[key] == "object") {
target[key] = clone(source[key]);
}else {
target[key] = source[key];
}
}
}
}
return target;
}
ここでは、マルチレイヤー オブジェクト配列がテストに使用され、ディープ コピーのプロパティが変更されて、元のデータに影響を与えるかどうかが確認されます。
let e = [{a:'1',b:'2',c:'3'},{aa:'11',bb:'22',cc:'33'},{aaa:'111',bbb:'222',ccc:'333'}];
let f = clone(e);
f[2].ccc='444';
console.log(e);
console.log(f);
コピーされた配列の 3 番目の要素の ccc 属性を変更した後、元の配列は変更されておらず、ディープ コピーが正常に実装されたことがわかります。
- JSONディープコピー方式:
ディープ コピーは、システムに付属の JSON を使用して実装することもできます。
JSON.parse(JSON.stringify(a));
実際には、コピーされるオブジェクトはまず JSON 文字列に変換され、次にその文字列がオブジェクトに変換されます。JSON オブジェクトと文字列の変換原理は実際には再帰的走査ですが、これを 2 回行う必要があるため、再帰的メソッドのアルゴリズム効率は JSON 変換の 2 倍になります。
ただし、再帰メソッドと JSON を使用した深いコピーには、再帰関数が実行されるたびにメモリ空間が呼び出されるという欠点があるため、コピー対象のオブジェクトの深さが非常に大きい場合は、再帰関数を使用するとスタック爆発の問題が発生します。以下でこの状況をシミュレーションしてみましょう。
//构造指定深度和广度的对象
function createData(deep,num) {
var data = {};
var temp = data;
for(var i=0;i<deep;i++) {
temp.data = {};
temp = temp.data;
for(var j = 0;j<num;j++) {
temp[j] = j;
}
}
return data;
}
この createData 関数は、深さが deep、幅が num のオブジェクトを構築するために使用されます。たとえば、createData(3,3) の場合、作成されるオブジェクトは {data:{'0':0,'1':1,'2':2,data:{'0':0,'1':1 です。 、'2':2、データ:{'0':0、'1':1、'2':2、}}}}。このとき、createData を使用して深さ 10000 のオブジェクトを作成し、再帰メソッド clone() と JSON を使用してオブジェクトをコピーすると、メモリ オーバーフロー エラーが発生します。
let d = createData(10000,0);
let e = clone(d);
console.log(e);
let d = createData(10000,0);
let e = JSON.parse(JSON.stringify(d));
console.log(e);
多くのレベルを持つオブジェクトに遭遇した場合、これら 2 つの方法は機能しないことがわかります。次に、メモリ オーバーフローの問題を効果的に解決できる 3 番目の方法である循環再帰ディープ コピーを紹介します。
2. ループトラバーサル方法:
ループを使用してオブジェクトのすべてのレベルのプロパティをトラバースできれば、関数を再帰的に呼び出すときに占有されるメモリ領域を節約でき、スタック爆発の問題を解決できます。では、ループのアイデアを使用してマルチレベルの走査を実現する方法を説明します。データ構造を見てみましょう
var a = {
a1: {
a11: 1,
},
a2: {
b1: 1,
b2: {
c1: 1
}
}
}
a オブジェクトは入れ子になったオブジェクトで、縦方向に見るとツリー状のデータ構造になっていることがわかります。
a
/ \
a1 a2
| / \
a11 b1 b2
| | |
1 1 c1
|
1
オブジェクト内の参照データ型の属性をツリー内の子ノードとして、基本データ型の属性をリーフ ノードの親ノードとして考えることができます。ディープ コピーを実現するには、このツリーの各ノードをトラバースするだけでよいことがわかります。このツリーをループで走査するには、スタックを使用して子ノード (リーフ ノードとその親ノードを除く) をこのスタックに配置し、これらのノードをコピーする必要があることを示す必要があります。各コピーは、スタックからノードを解放します。コピー中、スタックが空の場合、ツリーが走査されてコピーされたことを意味します。ループトラバーサルは基本的にスタック内の要素をループし、スタックの空のループは自然に終了します。スタック ループ トラバーサルを使用するプロセスをシミュレートしてみましょう。
ループする前に、まずこのスタックを構築します。スタックは後入れ先出しの特性を持つ配列です。トラバースする必要があるノードをスタックに入れる必要があります。最初にコピーを開始するときは、わかりません。コピーされるオブジェクトの属性が何であるかを確認するため、最初にシード データをスタックに置きます。スタックに格納される要素には、ノード情報を記述する 3 つの属性が必要です。親は、ループ内で走査される現在のノードである現在のノードを表します。キーは、属性名キーを持つ子要素を格納するために使用されます。 current ノードは、コピーする必要があることを示します。 sub-object、data は、サブオブジェクトのコンテンツがコピーされることを示します。
{
parent: root,
key: undefined,
data: x,
}
コピーされるオブジェクトを例として考えます。それをルートにコピーしたいとします。最初にルートの空のオブジェクトを構築し、次に上記のシード データをスタックに置きます。トラバースする前は、ルート ノードにいるため、親はルートです。コピーされたオブジェクトの構造がわからないため、キーは未定義であり、データはコピーされるコンテンツ、つまりオブジェクト全体を表します。
最初のループが開始され、シード データがスタックから取り出され、現在の親ノードの下のノードがループされます。a1 と a2 は参照データ型であり、コピーされるノードであることがわかります。スタックにはノード情報が格納されており、このとき親はルート、キーはa1、a2、データはそれぞれa1オブジェクト、a2オブジェクトとなります。a2 は a1 の後に格納されるため、a2 が最初にスタックからポップされます。
2 回目のループ: a2 配下のノードをループします。b2 は参照型で、b2 に対応するノード情報がスタックに格納されます。a2 配下で走査が行われるため、親は a2 になります。
ループの 3 ラウンド目: b2 がスタックからポップされ、b2 の下のノードを走査し、参照型がないことを検出して、それを直接コピーします。
ループの 4 ラウンド目: a1 がスタックからポップされ、a1 の下のノードを走査し、a11 が参照型ではないことを検出し、それを直接コピーします。この時点では、スタックは空であり、スタックにプッシュする必要がある参照型はありません。これは、走査が完了し、ループが終了したことを意味します。
このプロセスにより、ループの走査回数はコピーするデータ内の参照データ型の数に依存し、ネストされるオブジェクトの数はループの数に依存することがわかります。また、スタックの後入れ先出しの特性により、ループ トラバーサルも再帰的トラバーサルと同様、深さ優先のトラバーサルになります。つまり、最初に子要素にアクセスし、次に兄弟要素にアクセスします。
ディープコピーメソッドをループするコードを以下に掲載します。
//循环遍历对象实现深拷贝
function cloneLoop(x) {
const root = {};
// 栈 后进先出
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while(loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
ここでは便宜上、オブジェクトの状況のみを考慮していますが、配列の状況を考慮したい場合は、コピーの種類を判断し、その種類に応じて空のオブジェクトまたは空の配列を構築する必要があります。具体的な実装については、再帰メソッドを参照してください。
ループトラバーサルにより、再帰的なスタック爆発を解読し、ディープループコピーメソッド cloneLoop() を使用して、先ほど 10,000 層のオブジェクトをコピーできましたが、スタック オーバーフロー エラーが報告されていないことがわかり、この方法が有効であることがわかりました。
let d = createData(10000,0);
let e = cloneLoop(d);
console.log(e);
ループ トラバーサル方法は、階層内の多数のオブジェクトをコピーするときのメモリ オーバーフローの問題を解決できますが、依然としていくつかの特殊な状況には対処できません。循環参照オブジェクトやプロパティ間の参照関係を持つオブジェクトなど。例えば :
a={};
aa=a;
var obj = {};
var obj1 = {a1:obj, a2:obj};
a は循環参照オブジェクト、obj1 は属性間の参照関係を持つオブジェクトです。このようなオブジェクトの場合、上記の 3 つの方法のいずれも、ディープ コピー後に参照関係を維持することはできず、循環参照を解除することもできません。obj1 を使用して、これら 3 つの方法でコピー テストを実行すると、結果は次のようになります。
var obj = {};
var obj1 = {a1: obj, a2: obj};
console.log(obj1.a1 === obj1.a2) // true
var obj3 = cloneLoop(obj1);
console.log(obj3.a1 === obj3.a2) //false
var obj4 = JSON.parse(JSON.stringify(obj1));
console.log(obj4.a1 ===obj4.a2) //false
var obj5 = clone(obj1);
console.log(obj5.a1 === obj5.a2) //false
ここでは、循環参照と属性間の参照関係の問題を解決できる 4 番目の方法を紹介します。
3. 循環参照方式を廃止します。
4 番目の方法は、実際にはループ トラバーサル方法に基づく改良です。各サイクルを開始する前に、新しいオブジェクトが見つかった場合は、そのオブジェクトとそのコピーを保存します。オブジェクトの各コピーの前に、まずオブジェクトがコピーされているかどうかを確認します。コピーされている場合は、必要はありません。コピーする場合は、参照関係を維持できるように、元のものを直接使用します。
コピーされたオブジェクトを格納する配列 uniqueList を導入します。ループが通過するたびに、まず、今回コピーされるオブジェクトが uniqueList にあるかどうかを確認します。そうである場合、コピー ロジックは実行されず、最後にコピーされたオブジェクトが直接コピーされます。の参照がコピーされるプロパティに直接割り当てられます。それが存在しない場合は、最初に出現したオブジェクトがコピーされることを意味し、このオブジェクトとコピーされたオブジェクトの参照が uniqueList に格納されます。コードは以下のように表示されます。
/ 保持引用关系
function cloneForce(x) {
// =============
const uniqueList = []; // 用来去重
// =============
let root = {};
// 循环数组
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while(loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
// =============
// 数据已经存在
let uniqueData = uniqueList.find((item) => item.source === data);
if (uniqueData) {
parent[key] = uniqueData.target;
continue; // 中断本次循环
}
// 数据不存在
// 保存源数据,再拷贝数据中对应的引用
uniqueList.push({
source: data,
target: res,
});
// =============
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
改良されたループ トラバーサル メソッド cloneForce() を使用して obj1 をテストしたところ、属性間の参照関係がディープ コピー後もオブジェクト内に保持されていることがわかり、このメソッドが循環参照のクラッキングに有効であることがわかりました。
var obj = {};
var obj1 = {a1: obj, a2: obj};
console.log(obj1.a1 === obj1.a2) // true
var obj2 = cloneForce(obj1);
console.log(obj2.a1 === obj2.a2) // true
コピーされていない新しいオブジェクトは uniqueList 配列に入力する必要があるため、各コピーの前に uniqueList 配列を走査して、コピーされるオブジェクトが表示されるかどうかを確認する必要があります。属性には参照関係があるため、uniqueList 配列を走査するときに無意味な判断が行われ、アルゴリズムの効率が低下します。したがって、この方法は、データ量がそれほど多くなく、属性間の参照関係を維持する必要があり、この関係を満たす属性の数が多い場合にのみ適用できます。大量のデータを含むオブジェクトのコピーの場合は、参照関係を維持する必要がない 3 番目の方法を使用することをお勧めします。
以上がディープコピーを実現する4つの方法です。