Viki

Viki 写东西的地方

努力上进且优秀
x
github
email
bilibili

JavaScriptでのディープコピーとシャローコピーの実装を簡単に行います。

JS のデータ型#

JavaScript 言語では、プリミティブ型参照型の 2 つのデータ型があります。

プリミティブ型の変数名と値は、スタックメモリに格納されます。プリミティブ型の変数を宣言するたびに、変数名と値の格納のためにスタックメモリに新しい領域が割り当てられ、互いに影響を与えることはありません。

しかし、参照型は異なります。参照型の変数名と値は、スタックメモリに格納されますが、値は対応するヒープメモリアドレスです。実際の値はヒープメモリに格納され、スタックメモリには変数名とヒープメモリのアドレス情報が格納されます。

上記の参照型の値の割り当て方法は問題なさそうですが、実際の使用中に奇妙な状況が発生することがあります:

const obj = { a: 123, b: 456 }
const obj2 = obj
obj.a = 789
console.log(obj2.a)

出力は何だと思いますか?

関連する知識を知る前に、おそらく「123」と思うかもしれませんが、実際の答えは「789」です。見間違いではありません、本当に「789」です。F12 キーを押してコンソールで試してみてください。

しかし、なぜ「789」なのでしょうか?なぜ「123」ではないのでしょうか?

シャローコピーとディープコピー#

その理由は、参照型の変数に対して代入演算子 = を使用すると、JavaScript 言語の特性により、実際に代入されるのは変数 obj のアドレスです。(つまり、{ a: 123, b: 456} というオブジェクトがヒープメモリに格納されているアドレス)

理解できるように:= を使用すると、実際にはこのオブジェクトの「参照」がコピーされ、objobj2 は同じヒープメモリのアドレスを共有します(格納されているアドレスが同じ)。単純に参照アドレスをコピーし、実際には同じメモリを指しています。この方法を「代入」と呼びます。

上記の問題を解決するために、オブジェクトをコピーする必要があります。コピーの程度に応じて、シャローコピーディープコピーの 2 つの方法に分けられます。

シャローコピーは、新しいオブジェクトを作成し、プロパティがプリミティブ型の場合はその値をコピーします。プロパティがメモリアドレス(参照型)の場合は、メモリアドレスをコピーし、深い階層(1 階層以上)の参照型に対しては、メモリアドレスのみがコピーされます。

それに対して、ソースオブジェクトとコピーされたオブジェクトが相互に独立であり、どちらかのオブジェクトの変更が他のオブジェクトに影響を与えないようなコピー方法をディープコピーと呼びます。

シャローコピー#

以下の場合はシャローコピーに該当します(完全にコピーされず、新旧のオブジェクトが相互に影響を与える)

  1. 参照型の変数に対して直接代入演算子 = を使用する
  2. 配列の組み込みの sliceconcat メソッドなどを使用して 2 次元以上の配列を処理する
  3. Object.assign メソッドを使用してオブジェクトをコピーし、オブジェクトのプロパティ値に参照型が含まれている場合
  4. スプレッド演算子 ... を使用して 2 階層以上の参照型のデータ(オブジェクトまたは配列)をコピーする

ディープコピー#

以下の場合はディープコピーに該当します(完全にコピーされ、新旧のオブジェクトは互いに影響を与えません)

  1. 新しいオブジェクトを作成し、再帰的に各階層のプリミティブデータ型をコピーする
  2. JSON.parse(JSON.stringify(obj))(おすすめしません)
  3. jquerylodash などの組み込みのディープコピー関数

JSON.parse(JSON.stringify(obj)) を使用すると、いくつかの欠点があります:

  1. undefined関数Symbol の値は無視されます
  2. NaNInfinity-Infinity の値は null に設定されます
  3. ErrorRegExpSetMap などの組み込みオブジェクトは空のオブジェクトに変換されます
  4. Date オブジェクトは強制的に文字列に変換されます
  5. プロトタイプチェーン上のプロパティもコピーされます

ディープコピー関数の実装#

上記の奇妙な動作を解決するために、以下のような deepClone 関数が必要です:

const obj = { a: 123, b: 456 }
const obj2 = deepClone(obj)
obj.a = 789
console.log(obj2.a) // 789ではなく123が出力されます

最初のバージョンのコード:

function deepClone(obj, useJSON = false) {
  // プリミティブ型をフィルタリングする
  if (typeof obj !== 'object' || obj === null) return obj
  // JSON方式のディープコピーの実装
  if (useJSON) return JSON.parse(JSON.stringify(obj))

  let _obj = Array.isArray(obj) ? [] : {}
  for (const key in obj) {
    // プロトタイプチェーン上のプロパティはコピーしない
    if (!obj.hasOwnProperty(key)) return
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      // プロパティ値が参照型の場合、deepClone関数を再帰的に呼び出してディープコピーを行う
      _obj[key] = deepClone(obj[key])
    } else {
      // プリミティブ型は代入演算子を使用して直接コピーする
      _obj[key] = obj[key]
    }
  }
  return _obj
}

テストしてみましょう:

const obj = { a: 123, b: { c: 456 } }
const obj2 = deepClone(obj)
obj.b.c = 789
console.log(obj2.b.c) // 456が出力されます。テストに合格しました

上記の deepClone の実装は一般的な場合のディープコピーに対応できますが、一部のプロパティ値が組み込みの JavaScript オブジェクトである場合、状況はこれほど楽観的ではありません。次の例を見てみましょう:

const obj = {
  a: new Date(),
  b: { c: new Set([1]), d: new Map([['a', 1]]) },
  e: { f: new Error('msg'), g: new RegExp('^obj$') },
  h: e => console.log(e)
}

// 上記のdeepClone関数を使用します
const obj2 = deepClone(obj)
obj.b.c = e => console.log(e)
console.log(obj2)

// 出力結果は以下のようになります:
// {
//   a: {},
//   b: {
//     c: {},
//     d: {},
//   },
//   e: { f: {}, g: {} },
//   h: [Function: h],
// };

直感的にわかるように、SetMapErrorRegExpDate などの組み込みの JavaScript オブジェクトに対して、この関数は期待どおりに動作しません。これらの組み込みの JavaScript オブジェクトを通常のオブジェクトとして処理します。さらに改善して、これらの組み込みの JavaScript オブジェクトを元に戻す必要があります。

第 2 版のコード(最終版でもあります):

function deepClone(obj) {
  // タイプを取得する関数を定義します。Object.prototype.toString は詳細なタイプの説明を返します
  function getType(value) {
    return Object.prototype.toString.call(value).slice(8, -1)
  }
  // プリミティブ型と組み込みオブジェクト(Error、RegExp、Dateなど)をフィルタリングする
  if (getType(obj) !== 'Object' && getType(obj) !== 'Array') return obj

  let _obj = Array.isArray(obj) ? [] : {}
  for (const key in obj) {
    // プロトタイプチェーン上のプロパティはコピーしない
    if (!obj.hasOwnProperty(key)) continue
    if (getType(obj) === 'Object' || getType(obj) === 'Array') {
      // プロパティ値が配列またはオブジェクトの場合、deepClone関数を再帰的に呼び出してディープコピーを行う
      _obj[key] = deepClone(obj[key])
    } else {
      // プリミティブ型と組み込みの JavaScript オブジェクトは代入演算子を使用して直接コピーする
      _obj[key] = obj[key]
    }
  }
  return _obj
}

もう一度テストしてみましょう:

const obj = {
  a: new Date(),
  b: { c: new Set([1]), d: new Map([['a', 1]]) },
  e: { f: new Error('msg'), g: new RegExp('^obj$') },
  h: e => console.log(e)
}

const obj2 = deepClone(obj)
obj.b.c = e => console.log(e)
console.log(obj2)

// 出力結果は以下のようになります:
// {
//   a: 2022-03-06T05:11:33.838Z,
//   b: { c: Set(1) { 1 }, d: Map(1) { 'a' => 1 } },
//   e: {
//     f: Error: msg,
//     g: /^obj$/
//   },
//   h: [Function: h]
// }

第 2 版のコードはより科学的で正確な処理を行うため、Object.prototype.toString を使用してタイプを判断し、組み込みの JavaScript オブジェクトを元に戻しています。

ただし、このバージョンは通常のオブジェクトのコピーに対応しているだけで、Map、Set などの複雑なタイプのオブジェクトに対しては、コミュニティのソリューション(lodash、clone など)を使用してください。

2023/3/24 更新#

実際のプロダクション環境では、rfdcを使用することをお勧めします。これは、コミュニティのパフォーマンスの最適化された純粋なディープコピーの npm パッケージです。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。