javascriptで完璧にオブジェクトを複製したい!
JavaScriptでオブジェクトをコピーしようとした時、シャローコピーならスプレッド構文でなんとかなるんですが、ディープコピーしようとすると面倒だったのでいい方法を探したのですが、あまりいいものが見つからなかったので関数を作りました。
頑張ったので褒めてください。
jQueryのextendsとやらでできるらしいですが、このためだけにjQueryを入れたくはないですしおすし。
Qiitaの記事を書くのは初めてなので、至らぬ点があるかもしれませんがどうかご容赦を。
ディープコピーって何?
Javascriptのオブジェクトは参照型です。
プロパティーが変更されると、そのオブジェクトを参照している変数がすべて影響を受けます。
そのため、元のオブジェクトのプロパティーを変更したくない場合、完全に別オブジェクトとして複製する必要があります。
その中で、一番浅い階層のみを複製するのがシャローコピー(Shallow Copy)、すべての階層を複製するのがディープコピー(Deep Copy)です。
const hoge = {
foo: "吾輩は猫である",
baz: {
abc: "名前はまだ無い"
}
};
const huga = hoge;
const piyo = { ...huga }; // <= スプレッド構文(シャローコピー)
huga.foo = "ねこです。よろしくおねがいします。";
hoge.foo // => "ねこです。よろしくおねがいします。" <= ただの代入だと参照元も影響を受ける
huga.foo // => "ねこです。よろしくおねがいします。"
piyo.foo // => "吾輩は猫である" <= きちんとコピーできている
hoge === huga // => true
hoge === piyo // => false
huga.baz.abc = "ねこはいます。";
hoge.baz.abc // => "ねこはいます。"
huga.baz.abc // => "ねこはいます。"
piyo.baz.abc // => "ねこはいます。" <= 上書きされてしまっている
hoge.baz === huga.baz // => true
hoge.baz === piyo.baz // => true
JSON.parse(JSON.stringify())
一旦JSON(文字列)に変換してからJSのオブジェクトに戻す方法です。
おそらく一番簡単なディープコピー方法ですが、undefined, Symbol, Getter/Setter等JSONで表現できないものはコピーできません。
const 複製したオブジェクト = JSON.parse(JSON.stringify(コピー元オブジェクト));
Object.create()
prototypeを指定してオブジェクトを作成できるやつです。
元オブジェクトをprototypeにしてオブジェクトを作るということですね。
DevToolなどで見るとプロパティーがありませんが、きちんとprototypeからアクセスできます。
コピー先を変更しても、コピー元オブジェクトはも変わりません。
が、コピー元オブジェクトが変更されるとコピー先も変更を受けます。
コピー元がイミュータブルである場合には使えるのかもしれません。
const 複製したオブジェクト = Object.create(コピー元オブジェクト);
作る
いい感じにコピーできる手段が見つからなかったので、作ります。
最初からごちゃごちゃさせても分かりづらいので、とりあえず単純に。(単純じゃない気がする)
プロパティーのキーを取得してreduceで値を追加していきます。
Object.keys
だと一部プロパティーが取得できないのでObject.getOwnPropertyNames
とObject.getOwnPropertySymbols
を使用します。
※追記: Object.getOwnPropertyDescriptors
は全てのkeyを取得できるみたいなのでこれを使ってもいいかも。
reduceの第二引数(初期値)は、prototypeだけ継承したオブジェクトを指定します。
const clone = (object) => {
if(typeof object !== "object") return object;
const propNames = Object.getOwnPropertyNames(object);
const symbols = Object.getOwnPropertySymbols(object);
const prototype = Object.getPrototypeOf(object);
return [...propNames, ...symbols].reduce((propertiesObject, propName) => {
// プロパティーの取得
const prop = Object.getOwnPropertyDescriptor(object, propName);
// getter/setterでなければ再起呼出し
if(prop.hasOwnProperty("value"))
prop.value = clone(prop.value);
// プロパティーを追加
Object.defineProperty(propertiesObject, propName, prop);
return propertiesObject;
}, Object.create(prototype));
};
配列や他のオブジェクトにも対応させましょう。
typeof演算子ではオブジェクトは全て"object"になってしまうので、Object.prototype.toString
を使った関数を定義しています。
プリミティブ型のオブジェクトはオブジェクトのまま返していますが、単純なプリミティブ型を帰す場合はreturn new...
を消してください。
DateやMap等は同じように複製できないので個別に処理をしています。
const typeOf = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
const clone = (object) => {
if(Array.isArray(object)) // [], new Array
return object.map(clone);
if(typeof object === "object") switch(typeOf(object)) {
default: // new Foo etc...
case "object": { // {}, new Object
// 上と同じなので省略
}
case "number": // new Number
return new Number(object);
case "string": // new String
return new String(object);
case "boolean": // new Boolean
return new Boolean(object);
case "bigint": // Object(BigInt())
return object.valueOf();
case "regexp": // /regexp/, new RegExp
return new RegExp(object);
case "null": // null
return null;
case "date": return new Date(object);
case "map": {
const map = new Map();
for(const [key, value] of object)
map.set(key, clone(value));
return map;
}
case "set": return new Set(object);
}
// primitive type, function
return object;
};
完成?
こんな感じになりました。
existingObjects
にこれまでcloneしたオブジェクトを入れて再帰参照があったらエラー吐いてます。
ただ、これでも一部のオブジェクトは完璧にはコピーできないのですがね。
クロージャーで作成したような隠しパラメータを持っているようなのがだめっぽい気がします。
完璧にはテストできてないので不具合とかあるかもしれない。
const typeOf = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
const clone = (object, existingObjects) => {
if(!existingObjects) existingObjects = [];
else if(existingObjects.indexOf(object) !== -1)
throw new Error("Recursive reference exists.");
else existingObjects = [...existingObjects, object];
if(Array.isArray(object)) // [], new Array
return object.map(value => clone(value, existingObjects));
if(typeof object === "object") switch(typeOf(object)) {
default: // new Foo etc...
case "object": { // {}, new Object
const symbols = Object.getOwnPropertySymbols(object);
const propNames = Object.getOwnPropertyNames(object);
const prototype = Object.getPrototypeOf(object);
return [...propNames, ...symbols].reduce((propertiesObject, propName) => {
const prop = Object.getOwnPropertyDescriptor(object, propName);
if(prop.hasOwnProperty("value"))
prop.value = clone(prop.value, existingObjects);
Object.defineProperty(propertiesObject, propName, prop);
return propertiesObject;
}, Object.create(prototype));
}
case "number": // new Number
return new Number(object);
case "string": // new String
return new String(object);
case "boolean": // new Boolean
return new Boolean(object);
case "bigint": // Object(BigInt())
return object.valueOf();
case "regexp": // /regexp/, new RegExp
return new RegExp(object);
case "null": // null
return null;
case "date": return new Date(object);
case "map": {
const map = new Map();
for(const [key, value] of object)
map.set(key, clone(value, existingObjects));
return map;
}
case "set": return new Set(object);
}
// primitive type, function
return object;
}
ヲマケ(シャローコピー)
簡単なシャローコピーならスプレッド構文やObject.assign()
で実現できます。
ただしこの方法は、Getter/Setterがコピーできません。
const hoge = {
foo: "いろはにほへと ちりぬるを",
baz: "わかよたれそ つねならむ",
bar: "うゐのおくやま けふこえて",
qux: "あさきゆめみし ゑひもせす"
};
const huga = { ...hoge };
const piyo = Object.assign({}, hoge);
hoge === huga // => false
hoge === piyo // => false
Getter/Setterもコピーしたければディープコピーの以下の部分とclone
の再帰呼び出しを消しましょう。
if(!existingObjects) existingObjects = [];
else if(existingObjects.indexOf(object) !== -1)
throw new Error("Recursive call exists.");
else existingObjects = [...existingObjects, object];
参考
- JavaScriptの型などの判定いろいろ
- https://qiita.com/amamamaou/items/ef0b797156b324bb4ef3
- Object.assign()を使ったコピーいろいろ
- https://qiita.com/SE-studying-now/items/ecdbc0317ba1806aed61
- Object をコピーしたいマンの悩みとその解決法
- https://qiita.com/delphinus/items/edb963135e2ed176460b
- Object.create() - MDN - Mozilla
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/create