当サイトは、アフィリエイト広告を利用しています

jsのオブジェクトコピー方法~分割代入とスプレッド構文~

作成日:2022月12月25日
更新日:2023年12月11日

javascriptのオブジェクトのコピーに詳しく調べてみた
Reactでよく使う分割代入やスプレッド構文にもかかわってくるので
まとめておく。
javascriptのオブジェクトのコピーには大きく

  • 単純代入
  • シャローコピー
  • ディープコピー

の3種類がある。
この3つはコピー自体は行うが、内容が違うので
知らないまま行うとバグの原因になり得る。

javascriptのデータ型

コピー方法をまとめる前に前提となる
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 = 2
userObj.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]

上記のように他の変数に代入しなくても値が変更できることがわかる

コピーについて

オブジェクト型のコピーには単純代入とシャローコピーとディープコピーの三つがある。
※プリミティブ型については別変数に代入した時点で別のアドレスに格納されるため、
全部ディープコピーになる

単純代入

オブジェクト型を単純代入した場合はコピー元とコピー先が参照するアドレスは
同じになるため、一方を変更した場合、その変更は全てもう一方にも反映される。 ※反映というか同じアドレスを見てるので当然か~

index.ts
// 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を使う

Object.assignでシャローコピーした場合の結果を確認する

index.ts
// オブジェクトを定義
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階層目のみ

sample.ts
// 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階層目は分割代入元オブジェクトを参照しているため
代入元を変更すると分割代入した変数も値が変わってしまう

index.ts
// オブジェクトを定義
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段階のプロパティや、ネストしているオブジェクトの値を直接、
分割代入する場合は別アドレスになるので問題ない

index.ts
// オブジェクトを定義
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"

分割代入元のオブジェクトの値を変更しても
分割代入先の変数には影響しない

filterなどの処理

mapやfilterなどの処理で取得した結果もシャローコピーのため
取得元を変更した場合は取得先の値も変わる。
※ネストしている場合など

index.ts
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で取得した値も変わる。

ディープコピー

ディープコピーは完全にコピー元とは別のアドレスを確保するので
コピー元への変更はコピー先へは反映されない。   ディープコピーは三つのやり方がある

JSONのstringifyとparseを使う

sample.ts
// 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はオブジェクトを文字列に
変換してから再度、オブジェクトに戻すという手順を取っているため
文字列に変換できない

  • undefined
  • 関数

があった場合は情報が落ちる

lodash

lodashのcloneDeepを使えばディープコピーをすることができる

sample.ts
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を使う

structuredCloneメソッドを使う。

sample.ts
// 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)」

まとめ

  • オブジェクト型は明示的にディープコピーをしない場合は、基本、シャローコピーになる
  • シャローコピーでネストしたオブジェクト等を扱う時は気を付ける。
    一階層目しかコピーされず、二階層目からはコピー元参照になるため
  • ディープコピーする場合はlodashを使ったほうが安全   JSONメソッドやstructuredCloneを使う場合、値が落ちる可能性があるため

参考

下記を参考にさせて頂きました。 シャローコピー・ディープコピーとは
JavaScriptにおけるシャローコピーとディープコピーの解説とディープコピーにする方法
structuredCloneを使ってオブジェクトをディープコピーする

新着記事

タグ別一覧
top