シャローコピー・ディープコピーとは
始め
「スプレッド構文はシャローコピーだよ〜」と言われたことがありますが、正直シャローコピーが何なのか分からなかったので、簡単にまとめてみたいと思いました。
1. Javascriptのデータタイプ
コピーの話をする前に、まずはJavascriptにおいてデータの話を少しだけします。
1-1. プリミティブタイプ
プリミティブはオブジェクトでなく、メソッドを持たないデータのことで、6種類があります。
- string
- number
- BigInt
- boolean
- undefined
- symbol
そしてすべてのプリミティブ値は変更できません。簡単な例を見てみましょう。
let string = "apple";
console.log(string); // apple
string.toUpperCase();
console.log(string); // apple
const newString = string.toUpperCase();
console.log(newString) // APPLE
このようにプリミティブ値は操作した値を他の変数に入れることはできますが、直接変更ことはできないということが分かります。
1-2. オブジェクトタイプ
オブジェクトタイプには、以下のようなタイプがあります。
- array
- object
- function
- RegExp
変更できなかったプリミティブ値とは違って、オブジェクトタイプは直接変更することができます。
const array = [];
array.push("apple");
console.log(array); // ["apple"]
const object = { name: "apple" };
object.name = "banana"
console.log(object) // {name: "banana"}
constでも配列とオブジェクトの中身は変更できますね、これを書きながら初めて知りました。まるごと変更はもちろん不可能です。
const array = [];
array = ["apple"] // TypeError: "array" is read-only
データタイプについて簡単に説明させていただきました。これからはコピーの話をします!…と言いたいところですが、もう一つだけ短く紹介します。
2. Javascriptのコピーメカニズム
コピーの種類を調べる前にそもそもコピーでどう行われているかを話します。
2-1. プリミティブタイプ
Javascriptで変数などを宣言したら、その変数はパソコンのメモリに保存されます。プリミティブタイプの変数はコピーする際に新しいメモリ空間を確保して独立的な値を保存します。
簡単な例をあげてみます。
let string = "りんご";
let newString = string;
newString = "apple"
console.log(string, newString); // りんご apple
stringをコピーしてnewStringという変数を生成しました。そしてnewStringの値をいじりました。それでも原本のstringには何の影響もないです。なぜなら違うメモリに保存されているからです。
2-2. オブジェクトタイプ
しかし、オブジェクトタイプは少し違います。プリミティブタイプのように新しいメモリに保存するのではなく、原本のメモリアドレスを渡されます。つまり、原本とコピー本が同じメモリに保存されている同じデータを共有するということです。
const object = { name: "apple" };
const newObject = object;
newObject.name = "banana"
console.log(object); // {name: "banana"}
console.log(newObject); // {name: "banana"}
プリミティブタイプと全く同じ例をオブジェクトで試してみたらわかりやすいです。きっとnewObjectというコピー本をいじったのに、原本のobjectのnameまで変更されています。なぜなら同じデータを共有しているからです。
ここまで理解したら、オブジェクトタイプのデータは絶対あのようにコピーしてはいけないとすぐ納得がいきます。そしてオブジェクトタイプのコピー方法が気になります。
ここでやっとシャローコピーとディープコピーが登場します。オブジェクトタイプのコピーにはこの2種類があります。
3. オブジェクトタイプのコピー
3-1. シャローコピー
シャローコピーは名前通り浅い(shallow)コピーです。「浅い」が何を意味するかというと、1段階までコピーするという意味です。よく使われているスプレッド構文の例を見てみましょう。
const object = { name: "apple" };
const newObject = { ...object };
newObject.name = "banana";
console.log(object); // {name: "apple"}
console.log(newObject); // {name: "banana"}
上記と同じ例ですが、今回は無事に原本を破壊しないコピー本が作れました。しかし、これならどうでしょう?
const object = {
name: "apple",
like: {
food: "かぼちゃ"
}
};
const newObject = { ...object };
newObject.name= "banana";
newObject.like.food = "魚";
console.log(object); // { name: "apple", like: { food: "魚" } }
console.log(newObject); // { name: "banana", like: { food: "魚" } }
なぜかobjectのlikeまで魚になってしまいました。これは、シャローコピーでコピーしたからname(1段階)はコピーできてもオブジェクトの中のオブジェクトであるlikeの値(2段階)は原本と同じメモリアドレスを共有しているからです。
これが浅いコピーであるシャローコピーです。
シャローコピーをする他の方法ではObject.assign()というメソッドを利用する道もありますが、今はスプレッド構文がメージャーになっているようです。(アサイン…浅い…?)
3-2. ディープコピー
ディープコピーは簡単です。内部に存在するすべての値を全部コピーします。ですが、ディープコピーのし方は簡単ではありません。内部の値をの何もかも全部コピーするためにはすべての階層に渡って再帰的に値をコピーする必要があります。面倒くさいです。
ぐぐったら簡単な裏技をすぐ見つけられます。
const object = {
name: "apple",
like: {
food: "かぼちゃ"
}
};
const newObject = JSON.parse(JSON.stringify(object));
newObject.name = "banana";
newObject.like.food = "魚";
console.log(object); // { name: "apple", like: { food: "かぼちゃ" } }
console.log(newObject); // { name: "banana", like: { food: "魚" } }
原本のオブジェクトをJSON.stringify()でまず文字列化して、それをJSON.parse()でまたオブジェクト化するげんりです。
割と簡単ですが、この方法はプロパティにDateオブジェクトや関数、undefinedなどが入ってる場合は上手く動かないです。
これの他にはLodash(Javascriptのユーティリティライブラリ)にあるcloneDeep()メソッドを使う方法などがあります。
終わり
ディープコピーをする場合はそこまで多くないので、基本的にはシャローコピーをするのが普通だと思います。それでも、この内容知らなかったら後でバグの原因になるかもしれません。
Discussion