📘

JavaScriptの代入、シャローコピー、ディープコピーについて

2024/12/06に公開

JavaScriptでオブジェクトや配列を扱う際に、代入、シャローコピー、ディープコピーを理解して使い分けられていないコード、若手エンジニアのコードレビューしてると結構見かけるなと思ったので、具体的なコードを用いつつなるべく簡単にまとめてみます。

※若手エンジニア向けに簡単に説明する為の内容なので、細かい事は色々と割愛してます。中級者を目指す人向けの、ランタイムメモリ等も絡めたもうちょっと細かい解説も、後々書ければなと思います。

代入(単純代入)

イコール(=)を使った一番よく使われるものですね。

const obj1 = {
  key1: "val1",
  key2: "val2"
};
const obj2 = obj1;

// ※1
console.log(obj1 === obj2); // true

// ※2
obj2.key1 = "editVal1";
console.log(obj1); // {key1: "editVal1", key2: "val2"}
console.log(obj2); // {key1: "editVal1", key2: "val2"}

※1
obj2はobj1の値のコピーではなく、obj1を参照します。
そのため比較した結果はtrueとなります。

※2
obj1とobj2はイコールなので、どちらのkey1も変更されます。
obj2のkey1を変えたつもりが、obj1のkey1も変わってしまうのは、プログラミング初学者は大抵やってしまうミスでしょう。

シャローコピー(浅い複製)

手段は色々ありますが、ここではスプレッド構文を例に使用します。

const obj1 = {
  key1: "val1",
  innerObj: {
    innerKey1: "innerVal1",
    innerKey2: "innerVal2"
  }
};
const obj2 = { ...obj1 };

// ※1
console.log(obj1 === obj2);

// ※2
obj2.key1 = "editVal1";
console.log(obj1); // {key1: "val1", innerObj: {...}}
console.log(obj2); // {key1: "editVal1", innerObj: {...}}

// ※3
obj2.innerObj.innerKey1 = "editInnerVal1";
console.log(obj1.innerObj); // {innerKey1: "editInnerVal1", inner...}
console.log(obj2.innerObj); // {innerKey1: "editInnerVal1", inner...}

※1
シャローコピーしているので、値を比較するとfalseとなります。

※2
obj2の第一階層のkey1の値を変更しました。
単純代入の場合はobj1のkey1の値も変わってしまいましたが、シャローコピーのためobj1のkey1は変わっていません。

※3
obj2の第二階層innerObjのinnerKey1を変更しました。
シャローコピーでは第二階層以降はコピー元を参照する為、obj1の中のinnerKey1も変わってしまいます。

ディープコピー(深い複製)

ここではstructuredCloneを例に使用しますが、lodashのcloneDeepなども定番ですね。
JSON.stringify()からのJSON.parse()でもいいですが書くのが面倒・・・

const obj1 = {
  key1: "val1",
  innnerObj: {
    innerKey1: "innerVal1",
    innerKey2: "innerVal2"
  }
};
const obj2 = structuredClone(obj1);

// ※1
console.log(obj1 === obj2);

// ※2
obj2.key1 = "editVal1";
console.log(obj1); // {key1: "val1", innerObj: {...}}
console.log(obj2); // {key1: "editVal1", innerObj: {...}}

// ※3
obj2.innerObj.innerKey1 = "editInnerVal1";
console.log(obj1.innerObj); // {innerKey1: "innerVal1", innerKey...}
console.log(obj2.innerObj); // {innerKey1: "editInnerVal1", inner...}

※1、※2
当然ですがこれはシャローコピーの時と同様の結果になります。

※3
ディープコピーは第二階層以降も完全に別物になります。
obj2のinnerKey1を変更しても、obj1のinnerKey1は変わりません。

まとめ

3種類のオブジェクトのコピーのしかたを説明しましたが、それぞれの使い道は、元々のオブジェクトの階層構造や、オブジェクトのデータの使い方によってどれが最適解かは変わってきます。
まずはこの3つの違いを把握して実際にコーディングで使ってみて、最適解を導き出せるようになるのはまた次のステップで、かなと。
ディープコピーは完全に新しいメモリ領域を使うため、必要ないところでも何でもかんでもこれをしてしまうのは良くないですし、関数オブジェクトはディープコピー出来なかったりという制約もあったりしますが、これも後々中級者を目指す人向けの記事として書ければなと思います。(思うだけはタダなのでね・・・)

Discussion