この記事では、深いコピーの一般的な方法についてまとめています。銀の弾丸はなく、各方法には利点と欠点があり、使用する際にはシーンを区別する必要があります。
JavaScript では、深いコピーは古くから議論されてきた問題であり、何度もフロントエンドの JavaScript のスキルを評価するために面接官に使われてきました。また、開発中に深いコピーの理解が不十分な場合、見つけにくいバグが発生することもあります。以前に「JavaScript のクローンについての理解」についての記事を書いたことがありますが、その解決方法はまだ浅いです。この記事では、深いコピーの一般的な方法についてまとめています。銀の弾丸はなく、各方法には利点と欠点があり、使用する際にはシーンを区別する必要があります。
再帰的なコピー#
再帰的なコピーは、深いコピーの解決策を理解し始めたときに最初に出会う解決策です。その原理は、参照型の値をトラバースして、その値が参照型かどうかを判断し、参照型の場合はその関数を再帰的に呼び出してその値を渡し、単純なデータ型の場合は通常の代入コピーを実行することです。
function deepClone(obj) {
var result;
if (!(obj instanceof Object)) {
return obj;
} else if (obj instanceof Function) {
result = eval(obj.toString());
} else {
result = Array.isArray(obj) ? [] : {};
}
for (let key in obj) {
result[key] = deepClone(obj[key]);
}
return result;
}
これはシンプルな深いコピーのメソッドであり、オブジェクト、配列、関数を処理していますが、Date、Regexp、Map などの参照型は処理しておらず、オブジェクトの循環参照のシナリオを解決することもできません。
JSON.parse(JSON.stringify(object))#
一般的に、JSON.parse (JSON.stringify (object)) を使用するだけで深いコピーができます。
この方法の制限事項:
- undefined を無視する
- 関数をシリアライズできない
- 循環参照のオブジェクトをコピーできない
undefined と関数は無視され、循環参照のオブジェクトをコピーしようとするとエラーが発生します。
Uncaught TypeError:Converting circular structure to JSON
また、Map、Set、RegExp、Date、ArrayBuffer などの組み込み型はシリアライズする際に失われます。
ただし、通常の場合、複雑なデータはシリアライズできるため、この関数はほとんどの問題を解決できます。また、この関数はパフォーマンスの面でも最も速い深いコピーを提供する組み込み関数です。
構造化クローンアルゴリズム#
構造化クローンは、値を 1 つの場所から別の場所に移動するための既存のアルゴリズムです。たとえば、メッセージを別のウィンドウや WebWorker に送信するたびに使用されます。構造化クローンの利点は、循環オブジェクトを処理し、多くの組み込み型をサポートすることです。構造化クローンには MessageChannel と History API の使用が含まれます。
MessageChannel#
MessageChannel API を使用すると、新しいメッセージチャネルを作成し、その 2 つの MessagePort プロパティを介してデータを送信できます。
MessageChannel を作成し、メッセージを送信します。受信側では、メッセージには元のデータオブジェクトの構造化クローンが含まれています。
MessageChannel の postMessage で渡されるデータも深いコピーされます。これは Web Worker の postMessage と同じです。また、undefined や循環参照のオブジェクトもコピーできます。
// undefined + 循環参照あり
let obj = {
a: 1,
b: {
c: 2,
d: 3
},
f: undefined
};
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;
function deepCopy(obj) {
return new Promise(resolve => {
const { port1, port2 } = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
// MessageChannelは非同期です
deepCopy(obj).then(copy => {
let copyObj = copy;
console.log(copyObj, obj);
console.log(copyObj == obj);
});
ただし、関数を含むオブジェクトをコピーする場合、エラーが発生します。
Uncaught (in promise) DOMException:Failed to execute 'postMessage' on 'MessagePort': function() {} could not be cloned.
この方法の欠点は非同期であることですが、await を使用することで解決できます。
const clone = await structuralClone(obj);
History#
SPA を作成する際に history.pushState () を使用して URL を保存するために状態オブジェクトを提供することがあります。実際、この状態オブジェクトは構造化クローンを使用しており、同期的です。元の状態を復元するために、元の状態を復元した後に history.replaceState () を使用する必要があります。予期しない問題が発生しないようにするために、history.pushState () ではなく history.replaceState () を使用してください。
const structuralClone = obj => {
const oldState = history.state;
history.replaceState(obj, document.title);
const copy = history.state;
history.replaceState(oldState, document.title);
return copy;
};
この方法の利点は、循環オブジェクトの問題を解決でき、多くの組み込み型のクローンをサポートし、同期的であることです。欠点は、一部のブラウザが呼び出し頻度に制限を設けていることです。たとえば、Safari では 30 秒以内に 100 回しか呼び出すことができません。
Notification#
Notification はデスクトップ通知に使用されますが、JavaScript オブジェクトの深いコピーにも使用できます。
function structuralClone(obj) {
return new Notification('', {data: obj, silent: true}).data;
}
const obj = /* ... */;
const clone = structuralClone(obj);
この方法の利点は、循環オブジェクトの問題を解決でき、多くの組み込み型のクローンをサポートし、同期的であることです。欠点は、この API の使用にはユーザーの許可が必要ですが、データをクローンする場合はユーザーの許可なしに使用できます。HTTP プロトコルの場合、HTTPS のシナリオでのみ使用できます。何らかの理由で、Safari は常に undefined を返します。
lodash の深いコピー関数#
lodash の_.cloneDeep () は循環オブジェクトと多くの組み込み型をサポートし、多くの詳細を処理するのでおすすめです。
循環参照の解決#
上記の方法を分析すると、循環オブジェクトの問題を解決できるのは構造化クローンアルゴリズムだけです。では、ネイティブの JavaScript を使用してどのように解決できるでしょうか?ここでは、クロージャを使用して循環問題を解決する方法を試してみます。
function deepClone(object) {
const memo = {};
function clone(obj) {
var result;
if (!(obj instanceof Object)) {
return obj;
} else if (obj instanceof Function) {
result = eval(obj.toString());
} else {
result = Array.isArray(obj) ? [] : {};
}
for (let key in obj) {
if (memo[obj[key]]) {
result[key] = memo[obj[key]];
} else {
memo[obj[key]] = obj[key];
result[key] = clone(obj[key]);
}
}
return result;
}
return clone(object);
}
var obj = {};
var b = { obj };
obj.b = b;
var obj2 = deepClone(obj);
上記のコードでは、再帰的なコピーを最適化し、クロージャを使用して循環コピーの問題を解決しています。コピーされた参照アドレスを記録する memo 変数を使用して、再帰の前に再帰の引数値を記録することで、再帰内部で再帰を抜けることができ、循環参照の問題を解決できます。
まとめ#
- サイクルオブジェクトがなく、組み込み型を保持する必要がない場合は、クロスブラウザの JSON.parse (JSON.stringify ()) を使用して最速のクローンパフォーマンスを得ることができます。
- 適切な構造化クローンが必要な場合は、MessageChannel が唯一の信頼できるクロスブラウザの選択肢です。