当サイトは、アフィリエイト広告を利用しています
javascriptのオブジェクトのコピーに詳しく調べてみた
Reactでよく使う分割代入やスプレッド構文にもかかわってくるので
まとめておく。
javascriptのオブジェクトのコピーには大きく
の3種類がある。
この3つはコピー自体は行うが、内容が違うので
知らないまま行うとバグの原因になり得る。
コピー方法をまとめる前に前提となる
javascriptのデータ型について整理する。
プリミティブはメソッドをもたないnumberやboolなどの基本的な型のこと。
一度作成したら値を変更できないイミュータブル(immutable)の特性を持っている。
//値を設定let str:string ="hogefuge"str.substring(0,1)console.log(str)// 別の変数へ代入let strUp = str.substring(4,8)console.log(strUp)// ログ// hogefuge// fuge
上記のように変更した値を他の変数に入れることはできるのが
一度設定した変数を変更することはできない
オブジェクト型はプリミティブ以外のもの。
オブジェクト(連想配列)、配列、関数、正規表現、Dateなどはすべてオブジェクト型。
一度作成した後もその値自体を変更できるためミュータブル(mutable)の特性を持つ
※または参照型ともいう
// type宣言type User = {id:Number,name:string}// オブジェクト作成const userObj:User = {id:1,name:"tuji"}// オブジェクト変更userObj.id = 2userObj.name = "yamada"console.log(userObj)// 配列作成let array:number[] = Array(1,2,3,4,5)// 配列を編集array.splice(0,1)console.log(array)// ログ// {id: 2, name: "yamada"}// (4) [2, 3, 4, 5]
上記のように他の変数に代入しなくても値が変更できることがわかる
オブジェクト型のコピーには単純代入とシャローコピーとディープコピーの三つがある。
※プリミティブ型については別変数に代入した時点で別のアドレスに格納されるため、
全部ディープコピーになる
オブジェクト型を単純代入した場合はコピー元とコピー先が参照するアドレスは
同じになるため、一方を変更した場合、その変更は全てもう一方にも反映される。
※反映というか同じアドレスを見てるので当然か~
// type宣言type User = {id: Number;name: string;};// オブジェクト作成const userObj: User = {id: 1,name: "tuji"};// 単純代入でコピーconst copyObj = userObj;// コピー元オブジェクト変更userObj.id = 2;userObj.name = "yamada";console.log(`コピー元オブジェクト:${JSON.stringify(userObj, null, 3)}`);console.log(`コピー先オブジェクト:${JSON.stringify(copyObj, null, 3)}`);// コピー元オブジェクト:{// "id": 2,// "name": "yamada"// }// コピー先オブジェクト:{// "id": 2,// "name": "yamada"// }
コピー元を変更するとコピー先も変わる
コピー元の一部を参照にする代入方法
階層の一段目は別のオブジェクトになるが二段目以降はコピー元を参照している。
※浅いのは一段階目までしかちゃんとコピーしないってことか~
Object.assignでシャローコピーした場合の結果を確認する
// オブジェクトを定義const user = {id: 1,detail: {name: "hoge",age: 21}};// オブジェクトをシャローコピーconst copyUser = Object.assign({}, user);// コピー元オブジェクトを変更user.id = 99;user.detail.name = "yamada";console.log(`コピー元オブジェクト:${JSON.stringify(user, null, 3)}`);console.log(`コピー先オブジェクト:${JSON.stringify(copyUser, null, 3)}`);// コピー元オブジェクト:{// "id": 99,// "detail": {// "name": "yamada",// "age": 21// }// }// コピー先オブジェクト:{// "id": 1,// "detail": {// "name": "yamada",// "age": 21// }// }
2段階目のnameはコピー元を変更したことによって
コピー先も変わってしまっている。
スプレッド構文もシャローコピーのため
代入元オブジェクトを変更した場合はスプレッド構文で代入した値も変更されてしまう
※2階層目のみ
// userタイプを定義type User = { id: number; detail: { name: string; age: number } };// オブジェクトを定義const user: User = { id: 1, detail: { name: "hoge", age: 29 } };// スプレッド構文でコピー(シャローコピー)const copyUser = { ...user };user.id = 9999;// コピー元のnameを変更user.detail.name = "xxxxxxxxxx";console.log(`コピー元オブジェクト:${JSON.stringify(user, null, 3)}`);console.log(`コピー先オブジェクト:${JSON.stringify(copyUser, null, 3)}`);// コピー元オブジェクト:{// "id": 9999,// "detail": {// "name": "xxxxxxxxxx",// "age": 29// }// }// コピー先オブジェクト:{// "id": 1,// "detail": {// "name": "xxxxxxxxxx",// "age": 29// }// }
分割代入もシャローコピーのため 分割代入元オブジェクトを変更した場合は分割代入した値を変更されてしまう
ネストしているオブジェクトの2階層目のオブジェクトを
分割代入すると、2階層目は分割代入元オブジェクトを参照しているため
代入元を変更すると分割代入した変数も値が変わってしまう
// オブジェクトを定義const user = {id: 1,detail: {name: "hoge",age: 21}};// ネストしているdetailを分割代入するconst { detail } = user;// 分割代入元オブジェクトを変更user.detail.name = "yamada";// ログ出力console.log(`分割代入元オブジェクトのname:${user.detail.name}`);console.log(`分割代入したname:${detail.name}`);console.log(`オブジェクト:${JSON.stringify(user, null, 3)}`);console.log(`分割代入変数:${JSON.stringify(detail, null, 3)}`);// オブジェクト:{// "id": 1,// "detail": {// "name": "yamada",// "age": 21// }// }// 分割代入変数:{// "name": "yamada",// "age": 21// }
1段階のプロパティや、ネストしているオブジェクトの値を直接、
分割代入する場合は別アドレスになるので問題ない
// オブジェクトを定義const user = {id: 1,detail: {name: "hoge",age: 21}};// 1階層目と2階層目のname自体を分割代入const {id,detail: { name }} = user;// 分割代入元オブジェクトを変更user.id = 99;user.detail.name = "yamada";// ログ出力console.log(`オブジェクト:${JSON.stringify(user, null, 3)}`);console.log(`分割代入変数_id:${JSON.stringify(id, null, 3)}`);console.log(`分割代入変数_name:${JSON.stringify(name, null, 3)}`);// オブジェクト:{// "id": 99,// "detail": {// "name": "yamada",// "age": 21// }// }// 分割代入変数_id:1// 分割代入変数_name:"hoge"
分割代入元のオブジェクトの値を変更しても
分割代入先の変数には影響しない
mapやfilterなどの処理で取得した結果もシャローコピーのため
取得元を変更した場合は取得先の値も変わる。
※ネストしている場合など
type atrticle = { title: string; tags: string[]; cotent: string };// オブジェクトリストconst list: atrticle[] = Array.of({ title: "記事1", tags: ["java"], cotent: "aaaaaaaaaa" },{ title: "記事2", tags: ["javascript"], cotent: "bbbbbbbbbbb" },{ title: "記事3", tags: ["emotion"], cotent: "cccc" },{ title: "記事4", tags: ["javascript"], cotent: "ddddddddddddd" },{ title: "記事5", tags: ["css"], cotent: "eeeeeeeeeeeeeeeeeeeeeeeeee" },{ title: "記事6", tags: ["gatsby"], cotent: "f" });// オブジェクトリストからfilterで記事を抽出const newArticleList = list.filter((article) =>article.tags.find((tag) => tag === "gatsby"));// 抽出元の記事を変更list[5].title = "変更した";// 元記事出力list.forEach((at) => {console.log(`取得元記事のtitle:${JSON.stringify(at.title, null, 3)}`);});// filter記事出力newArticleList.forEach((at) => {console.log(`filterで取得した記事のtitle:${JSON.stringify(at.title, null, 3)}`);});// 取得元記事のtitle:"記事1"// 取得元記事のtitle:"記事2"// 取得元記事のtitle:"記事3"// 取得元記事のtitle:"記事4"// 取得元記事のtitle:"記事5"// 取得元記事のtitle:"変更した"// filterで取得した記事のtitle:"変更した"
filterの取得元の記事を変更すると、filterで取得した値も変わる。
ディープコピーは完全にコピー元とは別のアドレスを確保するので
コピー元への変更はコピー先へは反映されない。
ディープコピーは三つのやり方がある
// userタイプを定義type User = {id:number, detail: {name:string, age:number}};// オブジェクトを定義const user:User = {id: 1, detail: {name: 'hoge', age: 29}};// JSONメソッドでコピー(ディープコピー)const copyUser = JSON.parse(JSON.stringify(user))// コピー元のnameを変更user.detail.name = 'xxxxxxxxxx'// コピーを出力console.log(copyUser);// ログ// id: 1// detail: {// name: "hoge"// age: 29// }
コピー元が変わっても更新されていないが、注意としてはJSON.stringifyはオブジェクトを文字列に
変換してから再度、オブジェクトに戻すという手順を取っているため
文字列に変換できない
があった場合は情報が落ちる
lodashのcloneDeepを使えばディープコピーをすることができる
import * as _ from "lodash";// userタイプを定義type User = {id: number;detail: { name: string; age?: number; func: (param: string) => string };};// 関数を定義const func = (val: string) => `関数 : ${val}`;// オブジェクトを定義// ネストしたオブジェクトにundefinedと関数を設定するconst user: User = {id: 1,detail: { name: "hoge", age: undefined, func: func }};// lodashメソッドでコピー(ディープコピー)const copyUser = _.cloneDeep(user);// コピー元のnameを変更user.detail.name = "xxxxxxxxxx";// コピーを出力console.log(copyUser);// ログ// id: 1// detail: {// name: "hoge"// age: undefined// func: ƒ func() {}// }
関数とundefinedであるプロパティもコピーできている
structuredCloneメソッドを使う。
// userタイプを定義type User = {id: number;detail: { name: string; age?: number };};// オブジェクトを定義// ネストしたオブジェクトにundefinedと関数を設定するconst user: User = {id: 1,detail: { name: "hoge", age: undefined }};// structured-cloneメソッドでコピー(ディープコピー)const user_copy = structuredClone(user);// コピー元のnameを変更user.id = 9999;user.detail.name = "xxxxxxxxxx";// コピー元を出力console.log(`コピー元オブジェクト:${JSON.stringify(user, null, 3)}`);// コピー先を出力console.log(`コピー先オブジェクト:${JSON.stringify(user_copy, null, 3)}`);// コピー元オブジェクト:{// "id": 9999,// "detail": {// "name": "xxxxxxxxxx"// }// }// コピー先オブジェクト:{// "id": 1,// "detail": {// "name": "hoge"// }// }
コピー元を変更してもコピー先には影響していない。
ただstructuredCloneメソッドは関数はコピーできない。
下記のエラーが発生する
「Error: Failed to execute 'structuredClone' on 'Window': function func(val)」
下記を参考にさせて頂きました。
シャローコピー・ディープコピーとは
JavaScriptにおけるシャローコピーとディープコピーの解説とディープコピーにする方法
structuredCloneを使ってオブジェクトをディープコピーする