10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScriptにおけるシャローコピーとディープコピーの解説とディープコピーにする方法

Last updated at Posted at 2021-04-02

##はじめに
スプレッド構文についての投稿記事を作成していた時に、シャローコピー(浅い複製)、ディープコピー(深い複製)という聞きなれないワードが出てきて調べてみたので、シャローコピー、ディープコピーについて解説したいと思います。

##解説しないこと

  • JavaScriptにおける参照について
  • メモリについて

##シャローコピー、ディープコピーとは?
シャローコピーディープコピーとは、複製を作成時にコピー元を参照する深さの度合いによって変わる呼び名のことです。

JavaScriptは、プリミティブ以外はオブジェクトを単純代入で変数を作成した場合、参照先は、全て元のオブジェクトを指しています。

シャローコピーは、複製した物自体は複製元を参照しているが、別の入れ物として扱われており、複製元を一部参照する複製方法。

ディープコピーは、複製元を参照しない完全に独立した複製方法。

ここで強く言っておきたいのが、単純代入とシャローコピーとディープコピーは全て別の挙動をするということです。
たくさんの記事を見ましたが、7割くらいが単純代入とシャローコピーの挙動を一緒にしており単純代入のこともシャローコピーと書いてある記事をたくさん見ましたが、代入方法は3種類でそれぞれ挙動が違います。

*シャローコピーがなぜ存在するのか?どういった意図で使用するのか?というはかなり調べましたがたどり着けず今回は記載できませんでした。 申し訳ないです。。。 引き続き調査はしていきますので、分かり次第追記いたします。

##単純代入
複製元のメモリを、複製の全てのオブジェクトが参照する代入方法
####オブジェクト

copy.js
const obj1 = { a: 1, b: 2 }
const obj2 = obj1

console.log(obj1 === obj2) // true

obj2.a = 5
/* obj2はobj1を全て参照している為 1段目の変更でもobj1.aは変更されている */
console.log(obj1.a) // 5

####配列

copy.js
const array = [1,2,3,4,5]
array2 = array

console.log(array === array2)

array[0] = 6
console.log(array2) // Array [6, 2, 3, 4, 5]

##シャローコピー
複製元の一部を参照する代入方法
階層の一段目は別のオブジェクトとして扱われているが二段目以降は複製元を参照している。
####オブジェクト

copy.js
// オブジェクト
const obj = { a: 0, b: { c: 'c' }};
  
const shallowObj1 = Object.assign({}, obj);
/* 別のオブジェクトとして扱われる */
console.log(obj === shallowObj1) // false

/* 1段目は、コピーを変更してもコピー元は変わらない */
shallowObj1.a = 1
console.log(obj.a)     // 0

/* 1段目以降の変更はコピー元も変わってしまう */
shallowObj1.b.c = 'a'
console.log(obj.b.c)   // "a"

/* スプレッド構文で代入してもシャローコピーになる */
const shallowObj2 = {...shallowObj1}
console.log(obj === shallowObj2) // false

####配列

copy.js
// 配列
const array = [
  {a:1}
  {a:2}
  {a:3}
  {a:4}
]

const sharrowArray = array.map(item => item)
/* 別の配列として扱われている */
console.log(array === sharrowArray) // false

/* 配列の中のオブジェクトのプロパティを変更するとコピー元も変更されてしまう */
sharrowArray[0].a = 2
console.log(array[0].a) // 2


// 以下も同じシャローコピーの挙動になる
const copy1 = Array.slice(0);
const copy2 = Array.concat();
const copy3 = Array.filter(function () {
  return true;
});
const copy4 = Array.from(array);

##ディープコピー
複製元とは完全に別の独立した代入方法
####オブジェクト

copy.js
const obj1 = {
  a: 1,
  b: { c: 2},
}

/* 一度,文字列に変換して再度オブジェクトとして構築 */
const deepCopy = JSON.parse(JSON.stringify(obj1))
console.log(obj1 === deepCopy) // false

deepCopy.b.c = 3
console.log(obj2)  // Object { a: 1, b: Object { c: 2 } }


/* 但し、undefinedや関数が入ると削除される */
const obj2 = {
  a: 1,
  b: { c: 2},
  d: undefined
}

/* JSON.stringifyメソッドがundefinedを変換出来ない為,プロパティdが削除されてしまう */
const hasUndefined = JSON.parse(JSON.stringify(obj2))
console.log(hasUndefined) // Object { a: 1, b: Object { c: 2 } }

####配列

copy.js
const array1 = [
  { a: 1, b: 1, c: undefined},
  { a: 1, b: 1, c: undefined},
  { a: 1, b: 1, c: undefined},
]

const deepCopy = JSON.parse(JSON.stringify(array1))

console.log(array1 === deepCopy) // false

deepCopy[0].a = 2

console.log(array1[0].a) // 1

/* オブジェクト同様にundefinedが消えてしまう */
console.log(deepCopy) //Array [Object { a: 1, b: 1 }, Object { a: 1, b: 1 }, Object { a: 1, b: 1 }]

/* シャローコピーでもArray.prototype.map()を使ったが、配列の中の,オブジェクトの[構造]を返すとディ-プコピーになる */
const arrayMap = array1.map(item => ({ a: item.a, b: item.b, c: item.c }))

console.log(array1 === arrayMap) // false
console.log(arrayMap) // Array [Object { a: 1, b: 1, c: undefined }, Object { a: 1, b: 1, c: undefined }, Object { a: 1, b: 1, c: undefined }]

arrayMap[0].a = 2

console.log(array[0].a) // 1

##ライブラリを使う

copy.js
import _ from 'lodash'

const obj1 = {
  a: 1,
  b: { c: 2},
}

/* cloneDeepメソッドを使うとディープコピーで代入される */
const obj2 = _.cloneDeep(obj1)

##おまけ
スプレッド構文を使って、ディープコピーになるように工夫してみたが、あまり綺麗では無いので参考程度にしてください。。。

スプレッド構文についてはこちら

copy.js
const obj1 = {
  a: 1,
  b: { c: 2},
}

const obj2 = {
  ...obj1,
  b: {
    ...obj1.b
  }
}

obj2.b.c = 4
console.log(obj1.b.c) // 2

console.log(obj1 === obj2) // false

##まとめ
ディープコピーに関しては、最適解というものが無さそうでした。

個人的にはmap( )を使って構造自体を改めて返すという方法が制約も少なく使い勝手も良さそうだと感じてますが、とりあえずmap( )を使うのではなく、その時によって手法を選ぶというのが最適解な気がします。

以上、最後まで読んで頂きありがとうございました。

もし、間違っている箇所あればご指導、ご指摘よろしくお願いします。

##参考サイト

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?