はじめに
フォームの入力内容をリセットする処理を作成していた時の失敗談から調査した内容です。
問題の調査にあたり、これまでうやむやにしていたオブジェクトをコピーする際の周辺知識も調べました。
同じことでお困りの方や技術の振り返りをする際の参考になれば嬉しいです。
間違いやご指摘があれば、ぜひコメントをお願いします!
対象読者
- JavaScript始めたての方 🔰
- オブジェクトのコピーに関してうろ覚えの方 🤔
- 手っ取り早くオブジェクトをディープコピーする方法を知りたい方 💡
最初に結論
オブジェクトをディープコピーするには、structuredCloneが便利です!
書き方は以下の内容をご確認ください。
/**
* コピー対象
*/
const targetObject = {
title: "タイトル",
subtitle: "サブタイトル",
threshold: {
isUse: false,
value: 0
}
};
/**
* コピー方法
*/
const copyObject = structuredClone(targetObject);
この記事では、リセット処理の作成で失敗に至った経緯と、関連する知識について解説します。
起きたこと
私が簡単なフォーム作成のタスクを実施していたときのことです。
...
【 私のひとりごと 👀 】
「フォームの初期化とリセットで使ってるオブジェクト同じじゃん!共通化しよう!」
「できたから、リセットの動き確認して完了だ〜。」
(リセットボタン押下...)
「あれ、リセットできてない...?なんで?」
「オブジェクトって、1つを元にして複製できないんだっけ?今までどうしてたっけ?」
...
という具合に、ゴールを目の前にして止まりました笑。
この失敗で、時間を使ってしまったのですが、結果としてはちゃんと勉強し直すきっかけにもなりました。
調査の過程
- とりあえず、「js オブジェクト コピー」で方法を調べる。
- これまで言葉の意味を深く理解していなかった「ディープコピー」に出会う。
- ディープコピーの文献を読み進めると、「シャローコピー」の存在も知る。
- それぞれのコピー方法について学ぶ。
- 改めて振り返り、最適な方法で実装し直した結果、フォームが望んでいた動作をした。
この過程を経た調査結果を次項で紹介します。
結果
オブジェクトを代入した変数は、そのオブジェクトへの参照を格納する。
オブジェクトとプリミティブ(文字列・数字・真偽値などの基本的な値)のそれぞれのデータ型では、変数に格納する際の値の扱いが異なります。
- オブジェクトを代入した変数には、オブジェクト自体ではなく、そのオブジェクトが保存されている「メモリー上の場所(参照)」が記録される。
- プリミティブは、常に「値」を格納する。
プリミティブ
/**
* 対象の変数
*/
let primitive = "Hello";
/**
* コピーした変数
*/
let copyPrimitive = primitive;
/**
* 出力確認(代入直後)
*/
console.log(primitive); // 出力:Hello
console.log(copyPrimitive); // 出力:Hello
/**
* 出力確認(再代入後)
*/
primitive = "Good Morning"
console.log(primitive); // 出力:Good Morning
console.log(copyPrimitive); // 出力:Hello
それぞれ別の変数が作成されます。
変数primitive
の値を変更しても、copyPrimitive
の値に影響はないです。
オブジェクト
/**
* 対象の変数
*/
const object = {
name: "John",
};
/**
* コピーした変数
*/
const copyObject = object;
/**
* 出力結果(代入直後)
*/
console.log(object); // 出力:John
console.log(copyObject); // 出力:John
/**
* 出力結果(再代入後)
*/
object.name = "Ronaldo";
console.log(object); // 出力:Ronaldo
console.log(copyObject); // 出力:Ronaldo
プリミティブとは異なり、再代入後に両方の変数が同じ値になりました。
このように、プリミティブとオブジェクトで値の扱いが異なるため、作業の際は注意が必要です。
私はここを理解せず、用意した2つの変数に同じオブジェクトを格納したため、この状態に陥りました。
オブジェクトのコピー方法:「structuredClone」で安全かつ簡単に
JavaScriptでオブジェクトをコピーする方法は、大きく分けて「シャローコピー」と「ディープコピー」の2種類があります。
シャローコピー(Shallow copy)
オブジェクトの一番外側の構造だけをコピーし、内部のオブジェクトは元のオブジェクトへの参照をコピーします。
/**
* コピーしたい対象のオブジェクト
*/
const targetObject = {
title: "タイトル",
subtitle: "サブタイトル",
threshold: {
isUse: false,
value: 0
}
};
/**
* 方法1
* スプレッド構文を使用してコピーする
*/
const copyObj1 = { ...targetObject };
/**
* 方法2
* 組み込み関数Object.assignを使用してコピーする
*/
const copyObj2 = Object.assign({}, targetObject);
/**
* 検証
* オブジェクトの"第一階層"を更新する
*/
copyObj1.title = "title";
console.log(copyObj1.title); // 出力:title
console.log(copyObj2.title); // 出力:タイトル
/**
* 検証
* オブジェクトの"第二階層"を更新する
*/
copyObj1.threshold.value = "2";
console.log(copyObj1.threshold.value); // 出力:2
console.log(copyObj2.threshold.value); // 出力:2
オブジェクトの第一階層までは、コピーができます。
しかし、第二階層のvalue
は同じ値になりました。
このように、内部のオブジェクトは参照がコピーされる挙動となります。
一見不便そうで、「いつ使うの?」と思いがちですが、パフォーマンスを意識する際は、このシャローコピーを使う選択肢になりそうです。また、JSの組み込み関数でコピー操作を行うもの(*1)は、パフォーマンスの観点からシャローコピーを採用しているようです。
(*1:スプレッド構文、concat、slice、assign、など。)
ディープコピー(Deep copy)
オブジェクトの内部構造も含めて、完全に新しいオブジェクトをコピーします。
/**
* コピーしたい対象のオブジェクト
*/
const targetObject = {
title: "タイトル",
subtitle: "サブタイトル",
threshold: {
isUse: false,
value: 0
}
};
/**
* structuredCloneを使用する
*/
const copyObj1 = structuredClone(targetObject);
const copyObj2 = structuredClone(targetObject);
/**
* 検証
* オブジェクトの"第一階層"を更新する
*/
copyObj1.title = "title";
console.log(copyObj1.title); // 出力:title
console.log(copyObj2.title); // 出力:タイトル
/**
* 検証
* オブジェクトの"第二階層"を更新する
*/
copyObj1.threshold.value = "2";
console.log(copyObj1.threshold.value); // 出力:2
console.log(copyObj2.threshold.value); // 出力:0
シャローコピーと異なり、第二階層もそれぞれの値として取り扱いができるようになりました。
上記で紹介した方法以外に、JavaScriptのユーティリティであるlodashのcloneDeepを使う方法や、自前で関数を作成する方法があります。
ここでは、実装方法について割愛しますが、これらは複数行の記載が必要です。しかしながら、structuredCloneは、たった1行の記載で済みます。
以上のことから、ディープコピーを簡単にする場合は、structuredCloneがおすすめです。しかし、関数やSymbolなどはコピーができないようなので、取り扱う際は注意が必要でしょう。
結論
(いろいろ周辺知識を調べましたが...)オブジェクトを簡単にディープコピーするには、structuredCloneがおすすめです。
まとめ
- オブジェクトをそのまま変数に代入すると参照になってしまうため、元となる1つのオブジェクトをコピー(複製)して使い回すことができない。
- オブジェクトのコピーは「シャローコピー」と「ディープコピー」の2種類がある。
- シャローコピーは一番外側の構造だけをコピーし、内部のオブジェクトは元のオブジェクトへの参照をコピーする。反対に、ディープコピーはオブジェクトの内部構造も含めてすべてをコピーする。
- structuredCloneを使うと、オブジェクトを簡単にディープコピーできる。
参考文献
記事の作成あたり、下記のサイトを参考にさせていただきました。