深くネストされたObjectをDeep Copyする方法

# はじめに
開発インターンをしています川端([@haru1125632](https://twitter.com/haru1125632
))です。
PharmaXのアドベントカレンダーの15日目を担当します。
PharmaXではフロントはNext.js Typescriptを用いて作業しています
そこで今回はJavascriptにまつわる記事を書きました


# 概要
何がしたい?

### 1.以下のようにUserのObjectがある(ネストが深い)
```ts
const user = {
    name: {
        firstName: '太郎',
        lastName: {
            maidenName: '佐藤', // 旧姓
            marriedName: '田中', // 結婚後の名前
        }
    }
}
```

### 2. 何らかの方法でuserObjectをコピーしてotherUserObjectを作成し、otherObjectの一部を変えても、元のuserObjectに影響を及ばさないようにしたい。
```ts
const otherUser = ?; // ここでuserObjectを何らかの方法でCopyする
otherUser.name.lastName.maidenName = '川端';

// 佐藤が表示される
console.log(user.name.lastName.maidenName);
```

## 前提知識
### スプレッド構文や`Object.assign`の罠

```ts
const user = {
    name: {
        firstName: '太郎',
        lastName: {
            maidenName: '佐藤', // 旧姓
            marriedName: '田中', // 結婚後の名前
        }
    }
}

const otherUser = {...user}; // ここでuserObjectを何らかの方法でCopyする
otherUser.name.lastName.maidenName = '川端';

// 佐藤ではなく川端が表示されてしまう
console.log(user.name.lastName.maidenName);
```

`Object.assign`を使ったケースでも同じです。(最近はeslintでスプレットを使うように怒られることも多くなり頻出する回数は減ったように思われますが)

```ts
const user = {
    name: {
        firstName: '太郎',
        lastName: {
            maidenName: '佐藤', // 旧姓
            marriedName: '田中', // 結婚後の名前
        }
    }
}

const otherUser = Object.assign({}, user); // ここでuserObjectを何らかの方法でCopyする
otherUser.name.lastName.maidenName = '川端';

// 佐藤ではなく川端が表示されてしまう
console.log(user.name.lastName.maidenName);
```

スプレット構文や`Object.assign`はシャローコピー（浅いコピー)といえます

> オブジェクトのシャローコピーとは、コピーがコピー元のオブジェクトとプロパティにおいて同じ参照を共有する（同じ基礎値を指す）コピーのことを指します。その結果、コピー元とコピー先のどちらかを変更すると、もう一方のオブジェクトも変更される可能性があります。そのため、意図せずにコピー元やコピー先に予期しない変更が発生してしまう可能性があります。この挙動は、ソースとコピーが完全に独立しているディープコピーの挙動とは対照的です

https://developer.mozilla.org/ja/docs/Glossary/Shallow_copy

我々が今回やりたいのはディープコピーです

https://developer.mozilla.org/ja/docs/Glossary/Deep_copy

## スプレット構文で解決する方法

```ts
const user: User = {
    name: {
        firstName: '太郎',
        lastName: {
            maidenName: '佐藤', // 旧姓
            marriedName: '田中', // 結婚後の名前
        }
    }
}

const otherUser: User = {name: {...user.name, lastName: {...user.name.lastName}}}; // ここでuserObjectを何らかの方法でCopyする
otherUser.name.lastName.maidenName = '川端';

佐藤と表示される
console.log(user.name.lastName.maidenName);
```

可読性が悪い以下のようなCodeを作ってしまいました....また、辛すぎてTypescript使いました。
`const otherUser: User = {name: {...user.name, lastName: {...user.name.lastName}}};`

さらにネストが深くなると可読性が悪くなりそうですね。


# 本題: DeepCopyする

## 方法

自分の調べた限りだと３つの方法がありました。別の方法あれば教えていただけると幸いです

1. structuredClone()を使用する

2. JSONに変換してCopyする

3. 外部ライブラリを使用する


## 1. structuredClone()を使用する

聞き馴染みのない方も多いと思います。

https://developer.mozilla.org/ja/docs/Web/API/structuredClone

2022.12.10現在だと safariだと互換性がないらしいので浅く紹介したいと思います

```ts
const user: User = {
    name: {
        firstName: '太郎',
        lastName: {
            maidenName: '佐藤', // 旧姓
            marriedName: '田中', // 結婚後の名前
        }
    }
}

const otherUser: User = structuredClone(user); // ここでuserObjectを何らかの方法でCopyする
otherUser.name.lastName.maidenName = '川端';

// 佐藤と表示される
console.log(user.name.lastName.maidenName);
```

スプレット構文で無理やり実現した際よりも、だいぶ見やすいCodeになりました。やった！
ただ、safariで互換性がないとMDNで書かれている以上実際に運用はできないですね....はよ導入してくれ!!!!!!!




## 2. JSONに変換してCopyする

```ts
const user: User = {
    name: {
        firstName: '太郎',
        lastName: {
            maidenName: '佐藤', // 旧姓
            marriedName: '田中', // 結婚後の名前
        }
    }
}

const otherUser: User = JSON.parse(JSON.stringify(user)); // ここでuserObjectを何らかの方法でCopyする
otherUser.name.lastName.maidenName = '川端';

console.log(user.name.lastName.maidenName);
```

`const otherUser: User = JSON.parse(JSON.stringify(user))`;

幾分かはましですね。ただ、Dateが文字列になってしまったり、関数が消えてしまったり、undefinedが消えてしまったりするらしいです

https://qiita.com/seihmd/items/74fa9792d05278a2e898

MDNにもこう書いてありました

> 例えば、関数（クロージャ）、Symbols、HTML DOM API においてHTML要素を表すオブジェクト、再帰データ、その他の多くのケース。これらのケースにおいて JSON.stringify() を使用したオブジェクトのシリアライズは失敗します。つまり、それらのオブジェクトのディープコピーを作成する方法はありません


## 3. 外部ライブラリを使用する

今回はlodash使います、他にいいライブラリあったら教えてください。

https://lodash.com/docs/4.17.15#cloneDeep

```ts
import cloneDeep from "lodash.clonedeep";

const user: User = {
    name: {
        firstName: '太郎',
        lastName: {
            maidenName: '佐藤', // 旧姓
            marriedName: '田中', // 結婚後の名前
        }
    }
}

const otherUser: User = cloneDeep(user); // ここでuserObjectを何らかの方法でCopyする
otherUser.name.lastName.maidenName = '川端';

// 佐藤と表示される
console.log(user.name.lastName.maidenName);
```

const otherUser: User = cloneDeep(user);
これが良さそうだ!


# 個人的結論

structuredClone()がsafariに対応されるまで待ち(ただ、関数やundefinedがどのような扱いになるかは要注意)、それまでは外部ライブラリを使おう。もしlodashを使用するなら以下のようにする。

lodashに依存しすぎるのは良くないと思うので必要なmoduleだけinstallするようにする

https://www.npmjs.com/package/lodash.clonedeep