12
5

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-10

はじめに

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

概要

何がしたい?

1.以下のようにUserのObjectがある(ネストが深い)

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

2. 何らかの方法でuserObjectをコピーしてotherUserObjectを作成し、otherObjectの一部を変えても、元のuserObjectに影響を及ばさないようにしたい。

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

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

前提知識

スプレッド構文やObject.assignの罠

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でスプレットを使うように怒られることも多くなり頻出する回数は減ったように思われますが)

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はシャローコピー(浅いコピー)といえます

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

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

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

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する

方法

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

  1. structuredClone()を使用する

  2. JSONに変換してCopyする

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

1. structuredClone()を使用する

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

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

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する

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が消えてしまったりするらしいです

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

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

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

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

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するようにする

12
5
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
12
5