【JavaScript】真・オブジェクトのディープコピー

謎のプリン語る。
プログラミングの役立つ情報とか、どうでもいい雑談とか書いてます。
一人書く人増えました。

【JavaScript】真・オブジェクトのディープコピー

みやびプリン 500 316

500 320

【JavaScript】真・オブジェクトのディープコピー - サムネイル

※この記事は4年以上前の記事です。
現在は状況が異なる可能性がありますのでご注意ください。

あけおめことよろ。

ご無沙汰でした。
死んでません。生きてました。
ただ、新しいネタがあっても、書く気力と時間がなかっただけです。

さて、さっそく表記の件行ってみよう。

参照型言語である、JavaScriptにおいては、このディープコピーっていうのはたいそう難しい。
一番簡単なのは、JSON文字列に変換後に、再度オブジェクト化する方法だが、
他にも、再帰的にプロパティを潜っていき、新しくオブジェクトを作成し、再度新しい値の参照として同名プロパティを定義していく、という下記の方法がある。

function deepCopy(obj) {
  // 配列なら配列、と定義した方が手っ取り早い。
  const r = Array.isArray(obj) ? [] : {};
  // Object.keys(obj).forEachとかでもよし。ただし、entriesは、IEでは使えないので注意
  for (let key in obj) {
    // プロパティがオブジェクトなら、再帰的に自身を実行して潜っていく、値なら、そのまま代入
    r[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
  }
  return r;
}

だが、この方法、最大の欠点があり、それが多くのデベロッパーを苦しませた。
上記の方法だと、クラスメソッド(prototypeで定義したメソッド)は引き継がれないのである。

例えば下記のようなものは、オブジェクトとしてディープコピーはされるものの、
参照不可のクラス(プロトタイプ)メソッドは消滅してしまう。

// 上記deepCopyメソッドを、定義済みとする
class TestClass {
  constructor(hoge, mog) {
    this.hoge = hoge;
    this.mog = mog;
  }
  testFunc() {
    return this.hoge + this.mog;
  }
}
// IE対応するなら、下記になる
/*
const TestClass = function(hoge, mog){
  this.hoge = hoge;
  this.mog = mog;
};
TestClass.prototype.testFunc = function(){
  return this.hoge + this.mog;
};
*/

const testInstanse = new TestClass('ほげ', 'もぐ');
const testCopy = deepCopy(testInstanse);

console.log(testCopy.testFunc()); // Uncaught TypeError: testCopy.testFunc is not a function

これは、prototype構文を使ったとしても同様で、
(JSのクラスはprototypeの糖衣だからね)prototypeメソッドは、引き継げない。
おそらくオブジェクトのプロトタイプ汚染をなるたけ防ぐためと思われる。
だが、けっこうな場合は、インスタンスのコピーという形で、メソッドは実行したい、とはなるだろう。
実は、__proto__属性を使えば案外さっくり実装できるのだが、いかんせん、__proto__属性は現在非推奨となっている。(Object.prototype.__proto__ - JavaScript | MDN

メソッドのコピーは泣き寝入りするしかないか・・・?

そんなことはない。

Object.createをうまく使うことによって、汎用的にオブジェクトのディープコピーをすることができるのだ。
(先ほどの関数を少し変えている)

function deepCopy(obj) {
  const r = (() => {
    // 一応、余計な処理をいれないために、プロトタイプ名がObjectか、Arrayだったらそのまま返す
    if (obj.constructor.name === 'Array' || obj.constructor.name === 'Object') return Array.isArray(obj) ? [] : {};
    // プロトタイプ名が上記以外なら、Object.createをうまく使って、新しくインスタンスを生成して返す
    return Object.create(obj.constructor.prototype);
  })();

  // IEはアロー関数が使えないので、下記のようにするといいだろう。
  /*
  const r = (function(){
    // 中は同じ処理
  })();
  */

  // Object.keys(obj).forEachとかでもよし。ただし、entriesは、IEでは使えないので注意
  for (let key in obj) {
    // プロパティがオブジェクトなら、再帰的に自身を実行して潜っていく、値なら、そのまま代入
    r[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
  }
  return r;
}

Object.createメソッドは、第一引数のオブジェクトをプロトタイプとした新しいオブジェクトを返すメソッドだ。
よって、コピー元のオブジェクトのコンストラクターをプロトタイプとすることによって、同じプロトタイプからインスタンスを生成できる、って寸法だ。
あとは、従来の再帰実行を持って、各プロパティに潜って、値を別参照で定義していけばいい。

さぁ、これで、JSでの大規模開発も本格的にできるぜよ。
ではまた。

ローズオンライン - メイン

トラックバック(0)

トラックバックURL:

コメントする

ページトップへ戻る