はじめに
開発インターンをしています川端(@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つの方法がありました。別の方法あれば教えていただけると幸いです
-
structuredClone()を使用する
-
JSONに変換してCopyする
-
外部ライブラリを使用する
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するようにする