2017/05/30

ES6のObject.assignがシャローコピーなのでディープコピーする方法を考える

ES6(ES2015)で実装されたObject.assignでディープコピーできると思っていた時期が私にもあった。
Object.assignでディープコピーして(した気になって)、プロパティの値を変更したとき、元のオブジェクトにも影響していることに気づいた。

// Object.assign.js
const obj1 = {
  hoge: 'hoge',
  fuga: {
    foo: 'foo'
  }
};

// ディープコピーのつもり
const obj2 = Object.assign({}, obj1);

obj2.hoge = 'hogehoge';

// obj1.hoge: 'hoge'  obj2.hoge: 'hogehoge' ← ディープコピーされてる?
console.log('obj1.hoge:', obj1.hoge, 'obj2.hoge:', obj2.hoge);


obj2.fuga.foo = 'foofoo';

// obj1.fuga.foo: 'foofoo'  obj2.fuga.foo: 'foofoo' ←?!?! 
console.log('obj1.fuga.foo:', obj1.fuga.foo, 'obj2.fuga.foo:', obj2.fuga.foo);

プリミティブ型はコピーされているのだが、参照型(上記の例ではObject)では参照先がコピーされていた。


ライブラリを使わずにディープコピーする方法


JSON.stringifyとJSON.parseを使う方法(問題点あり)

stringifyでJSONをstring型(プリミティブ型)に変換するとで値渡しになり、それをparseすることでObject型に戻す方法だ。

※ C#やJavaでのStringは参照型だが、JavaScriptのstringはプリミティブ型になる
参照: データ構造 - JavaScript | MDN
// JSON.js
const obj1 = {
  hoge: 'hoge',
  fuga: {
    foo: 'foo'
  }
};

const obj2 = JSON.parse(JSON.stringify(obj1));

簡単にオブジェクトをコピーできるのだが、問題点もある。
プロパティにfunctionやundefinedがあると、そのプロパティ自体がなくなってしまう点だ。


ディープコピー関数を自作する方法

JavaScriptにはディープコピーするための関数が用意されていない。そのため、選択肢はディープコピー関数を自作するか、ライブラリを使うかになる。
せっかくなので、今回は自作してみる。

// DeepCopy.js
const object = {
  // プリミティブ型
  a: 1,
  b: 'a',
  c: '',
  d: null,
  e: undefined,
  f: true,
  // 参照型
  g: [1, 2, 3],
  h: function () { console.log('h'); },
  i: {
    a: 1,
    b: 'a',
    c: '',
    d: null,
    e: undefined,
    f: true,
    g: [1, 2, 3],
    h: function () { console.log('h'); },
    i: { a: 1 }
  }
};

function deepClone(object) {
  let node;
  if (object === null) {
    node = object;
  }
  else if (Array.isArray(object)) {
    node = object.slice(0) || [];
    node.forEach(n => {
      if (typeof n === 'object' && n !== {} || Array.isArray(n)) {
        n = deepClone(n);
      }
    });
  }
  else if (typeof object === 'object') {
    node = Object.assign({}, object);
    Object.keys(node).forEach(key => {
      if (typeof node[key] === 'object' && node[key] !== {}) {
        node[key] = deepClone(node[key]);
      }
    });
  }
  else {
    node = object;
  }
  return node;
}

const cloned = deepClone(object);

// Modify
object.a = 2;
object.b = 'b';
object.c = '_';
object.d = 'null';
object.e = 'undefined';
object.f = false;
object.g = [3, 2, 1];
object.h = function () { console.log('cloned'); }
object.i.a = 2;
object.i.b = 'b';
object.i.c = '_';
object.i.d = 'null';
object.i.e = 'undefined';
object.i.f = false;
object.i.g = [3, 2, 1];
object.i.h = function () { console.log('cloned'); }
object.i.i.a = 2;

// Check
console.log(object.a === cloned.a,         'object.a:',        object.a,     'cloned.a:',        cloned.a);
console.log(object.b === cloned.b,         'object.b:',        object.b,     'cloned.b:',        cloned.b);
console.log(object.c === cloned.c,         'object.c:',        object.c,     'cloned.c:',        cloned.c);
console.log(object.d === cloned.d,         'object.d:',        object.d,     'cloned.d:',        cloned.d);
console.log(object.e === cloned.e,         'object.e:',        object.e,     'cloned.e:',        cloned.e);
console.log(object.f === cloned.f,         'object.f:',        object.f,     'cloned.f:',        cloned.f);
console.log(object.g === cloned.g,         'object.g:',     ...object.g,     'cloned.g:',     ...cloned.g);
console.log(object.h === cloned.h,         'object.h:',        object.h,     'cloned.h:',        cloned.h);
console.log(object.i.a === cloned.i.a,     'object.i.a:',      object.i.a,   'cloned.i.a:',      cloned.i.a);
console.log(object.i.b === cloned.i.b,     'object.i.b:',      object.i.b,   'cloned.i.b:',      cloned.i.b);
console.log(object.i.c === cloned.i.c,     'object.i.c:',      object.i.c,   'cloned.i.c:',      cloned.i.c);
console.log(object.i.d === cloned.i.d,     'object.i.d:',      object.i.d,   'cloned.i.d:',      cloned.i.d);
console.log(object.i.e === cloned.i.e,     'object.i.e:',      object.i.e,   'cloned.i.e:',      cloned.i.e);
console.log(object.i.f === cloned.i.f,     'object.i.f:',      object.i.f,   'cloned.i.f:',      cloned.i.f);
console.log(object.i.g === cloned.i.g,     'object.i.g:',   ...object.i.g,   'cloned.i.g:',   ...cloned.i.g);
console.log(object.i.h === cloned.i.h,     'object.i.h:',      object.i.h,   'cloned.i.h:',      cloned.i.h);
console.log(object.i.i.a === cloned.i.i.a, 'object.i.i.a:',    object.i.i.a, 'cloned.i.i.a:',    cloned.i.i.a);

プロパティの値がArray型ならArray.prototype.slice()を使って、新しい配列をつくっていく。Object型ならObject.assignを使って再帰的にコピーしている。

これでディープコピーはできる。(テストが不十分なのでバグがあるかも)


ライブラリ(jQueryやlodashなど)を使う方法

多分これが一番楽で正確だと思います。

// jQuery
const newObj = jQuery.extend(true, {}, oldObj);

// lodash
const newObj = _.cloneDeep(oldObj);



参考サイト






以上

written by @bc_rikko

0 件のコメント :

コメントを投稿