🤖

[JavaScript]シャローコピーとディープコピーを理解する

2022/05/05に公開

はじめに

Safari15.4が出たことで全てのモダンブラウザでstructuredClone()を使いディープコピーができるようになりました。
社内でも話題に上がりましたが、そもそもディープコピーってなんだ・・・?となったので簡単に調べてまとめます。
またシャローコピーについても取り上げます。

JavaScriptのデータ型

シャローコピー・ディープコピーの話の前にまずJavaScriptのデータについての話を少しします。
JavaScriptはプリミティブ型とオブジェクト型の2つに分けられます。

プリミティブ型

真偽値や数値など基本的な値の型のことです。
この型は一度作成すると値自体の変更をできない、イミュータブルの特性があります。
詳細はこちら↓
https://developer.mozilla.org/ja/docs/Glossary/Primitive#javascript

プリミティブ型のコピー

プリミティブ型のコピーは作成されるのと同時にメモリ上に新しく値を割り当てます。
それぞれ別のメモリに値が存在するのでaをbに代入した後、aの値を変更してもbは変わりません。
プリミティブ型は値として複製される。

let a = 100;
let b = a;
a = 200;

console.log(a); // 200
console.log(b); // 100

オブジェクト

プリミティブ型ではないオブジェクトや配列などのことをオブジェクトと呼びます。
シャローコピー、ディープコピーが必要になるのはオブジェクトや配列などのデータだけ
https://developer.mozilla.org/ja/docs/Web/JavaScript/Data_structures#データと構造型

オブジェクトのコピー

オブジェクトのコピーはプリミティブ型と違い新しく値を作成しない参照渡しです。
変数personnewPersonはメモリ上の同じデータを見にいきます。同じデータに別名を付けていると理解しました。

const person = {name: "Tom"};
const newPerson = user;
person.name = "Bob";
console.log(person); // {name: "Bob"}
console.log(newPerson); // {name: "Bob"}

参照するデータが同じなので値を変更するとそれぞれ値が変わります。

ただ実際はオブジェクトの値が変わると困る場面が多いと思います。
そこでシャローコピー、ディープコピーが必要になります。

シャローコピー、ディープコピー

コピー元とコピー先の参照の深さに応じた呼び名のこと。

シャローコピー

シャローコピー(shallow copy)名前の通り浅いコピーでコピー元の1階層のみ複製します。
メモリ上にオブジェクトの1階層を複製しネストされたオブジェクトは参照を渡します。
シャローコピーはObject.assignやスプレッド構文で行います。

const person = {
  firstname: "Alex",
  lastname: "Turner",
  birth: {
    year: 1986,
    month: 1,
    date: 6
  }
}
const newPerson = { ...person };

newPerson.firstname = "Miles";
newPerson.lastname = "Kane";

console.log(person); // {firstname: "Alex", lastname: "Turner", birth{...}}
console.log(newPerson); // {firstname: "Miles", lastname: "Kane", birth{...}}

1階層目はシャローコピーによって複製されますが、2階層目のオブジェクトはコピー元、コピー先で同じメモリを参照するので片方の値を変えるともう片方も変わってしまいます。

const person = {
  // 省略
}
const newPerson = { ...person };

newPerson.firstname = "Miles";
newPerson.lastname = "Kane";

newPerson.birth.month = 3;
newPerson.birth.date = 17;

console.log(person);
// {firstname: 省略, birth: {year: 1986, month: 3, date: 17}}
console.log(newPerson);
// {firstname: 省略, birth: {year: 1986, month: 3, date: 17}}

ディープコピー

ディープコピーは参照ではなく値をコピーします。
メモリに格納されているデータを全てコピーするということです。

ディープコピーしてみる

1. JSON.parse(jSON.stringify(obj))

ググってよく出てくる方法です。

const person = {
  firstname: "Alex",
  lastname: "Turner",
  birth: {
    year: 1986,
    month: 1,
    date: 6
  }
}
const newPerson = JSON.parse(JSON.stringify(person));
newPerson.birth.month = 3;
newPerson.birth.date = 17;

console.log(person);
// {..., birth: {year: 1986, month: 1, date: 6}}
console.log(newPerson);
// {..., birth: {year: 1986, month: 3, date: 17}}

問題が発生する場合があるので注意が必要です。
https://qiita.com/seihmd/items/74fa9792d05278a2e898#undefined

2. lodashcloneDeep

2つ目はlodashを使うことです。
事前にインストールする必要がありますが、簡単に使うことができます。

import _ from "lodash";
const person = {
  // 省略
};
const newPerson = _.cloneDeep(person);
newPerson.birth.month = 3;
newPerson.birth.date = 17;

console.log(person);
// {..., birth: {year: 1986, month: 1, date: 6}}
console.log(newPerson);
// {..., birth: {year: 1986, month: 3, date: 17}}

3. structuredClone()

3つ目は今回シャローコピー、ディープコピーについて調べるきっかけになった関数です。
下記に現在コピーできる型が書いてあります。関数はできないようです。
https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm

const person = {
  // 省略
};
const newPerson = structuredClone(person);
newPerson.birth.month = 3;
newPerson.birth.date = 17;

console.log(person);
// {..., birth: {year: 1986, month: 1, date: 6}}
console.log(newPerson);
// {..., birth: {year: 1986, month: 3, date: 17}}

まとめ

シャローコピー、ディープコピーについてまとめました。
実務ではシャローコピーを使うことが多いのですが、今回調べた内容を知っておかないとハマってしまいそうだなと思いました。

  • シャローコピー
    オブジェクトの1階層目をコピーする。2階層目がある場合は参照渡しになる。
    Object.assignやスプレッド構文でできる。

  • ディープコピー
    オブジェクトの値そのものをコピーする。ディープコピーする方法によっては問題があるので注意。
    オブジェクトのプロパティに関数がなければstructuredClone()、関数なども含めたディープコピーをしたいのであればlodashcloneDeepを使うのが良さそう。

参考

https://jsprimer.net/basic/data-type/#data-type
https://jsprimer.net/basic/object/#copy

Discussion