banner
他山之石

他山之石

深いコピーの秘密

この記事では、深いコピーの一般的な方法についてまとめています。銀の弾丸はなく、各方法には利点と欠点があり、使用する際にはシーンを区別する必要があります。

画像

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 が唯一の信頼できるクロスブラウザの選択肢です。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。