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);
参考サイト
- Object.assignが実装された - JS.next
- JavaScriptのDeepCopyでJSON.parse/stringifyを使ってはいけない - Qiita
- Object.assign() - JavaScript | MDN
- データ構造 - JavaScript | MDN
以上
written by @bc_rikko
0 件のコメント :
コメントを投稿