この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。MDNの公式資料を確認し、架空の設定データでコピー方法の違いを比較する教材を作りました。検証ステータス:📘 MDN公式仕様確認・Linux Node.js 22.16.0で同等ロジック10条件PASS/GitHub保存版の再テスト・実ブラウザー・GitHub Pages・WordPress表示は未確認
「設定をコピーして編集しただけなのに、元の設定まで変わった」。JavaScriptでオブジェクトを扱うとき、そんな不思議な現象が起こります。原因は、外側だけコピーする方法と、入れ子のデータまでコピーする方法の違いです。今回は架空の設定を使い、ボタン一つで結果を比較します。
まず試す:元の設定は変わる?
| 項目 | 今回の条件 |
|---|---|
| 対象 | structuredClone() 対応のブラウザー、Node.js 18以降 |
| 使用技術 | JavaScriptのスプレッド構文、structuredClone、Date、Map |
| 費用・通信 | 標準APIのみ。CDN、APIキー、個人情報、外部送信なし |
| 実測 | Linux Node.js 22.16.0で純粋ロジックの10テストPASS。ブラウザー表示は未検証 |
GitHub Pages公開URLの到達・実動作は今回確認していません。確実にソースを確認する方法は、上の固定コミットから web/structured-clone-lab/ を取得することです。リポジトリのルートで python3 -m http.server 8000 を実行し、http://127.0.0.1:8000/web/structured-clone-lab/ を開く設計です。停止は Ctrl+C。実ブラウザー動作は未確認です。
操作は3つです。
「もう一度比較する」を押して、元データの値を確認する。
「浅いコピー変更後の元データ」が
redになることを観察する。「深いコピー変更後の元データ」は
redのまま、コピー側だけgreenになることを比べる。
注意:最初の浅いコピーの変更ですでに元データはredになっています。 深いコピーがそれを元に戻すわけではありません。深いコピーを変更しても、元データにさらに影響しないことを確かめます。
なぜ元データが変わるのか
次の例では {...original} が外側のオブジェクトを作ります。しかし、settings は同じ入れ子のオブジェクトを指します。
const original = {settings: {theme: "blue"}};
const shallow = {...original};
shallow.settings.theme = "red";
console.log(original.settings.theme); // "red"
一方、structuredClone() は対応する値を深く複製します。
const original = {settings: {theme: "blue"}};
const deep = structuredClone(original);
deep.settings.theme = "green";
console.log(original.settings.theme); // "blue"
この2つのコードはそれぞれ独立した例です。教材の比較ボタンでは、同じ元データに浅いコピーの変更を加えた後、深いコピー側を変更します。
flowchart TD
A["元の架空設定"] --> B["浅いコピー {...original}"]
A --> C["深いコピー structuredClone"]
B --> D["入れ子の参照を共有"]
C --> E["対応する入れ子も複製"]
D --> F["変更が元データに反映"]
E --> G["コピー側の変更は元に影響しない"]
DateとMapも比べる:JSON変換との違い
古いコードで見かける JSON.parse(JSON.stringify(value)) も、JSONとして表せるデータを別のオブジェクトにできます。しかし、元の型をすべて保持するわけではありません。
| データ | structuredClone | JSON文字列化→復元 |
|---|---|---|
| 入れ子の通常オブジェクト | 深く複製 | JSONで表せる範囲で複製 |
| Date | Dateとして保持 | ISO形式の文字列になる |
| Map | Mapとして保持 | この教材のMapは空のオブジェクトになる |
| 循環参照 | 対応 | JSON.stringify で通常エラー |
| 関数 | DataCloneError | プロパティが省略される場合がある |
つまり、「JSONでコピーできた」ことと「データ型を保ってコピーできた」ことは別です。今回の教材では Date と Map を固定入力にして、その差を文字で表示します。
1条件だけ変えてみる
最初は settings.theme という入れ子の値を変更します。次にコードをコピーして、入れ子ではない team だけを "架空のB班" に変更してみてください。浅いコピーでも、外側にある文字列プロパティの再代入は元データに影響しません。
この比較で「浅いコピーが常に危険」なのではなく、入れ子の共有を理解して使うことが大切だと分かります。変更を許さない設計、必要な部分だけのコピー、データ構造の単純化も選択肢です。
動かないときの切り分け
| 症状 | 原因候補 | 対処 |
|---|---|---|
structuredClone がない | ブラウザーや実行環境が古い | 対応状況を確認。Node.jsは18以降を想定 |
DataCloneError が出る | 関数やDOM要素など非対応の値 | 複製する対象を見直す |
| ボタンを押しても表示が変わらない | JavaScriptが無効、ファイル未読込 | clone.js と app.js のパスと開発者ツールを確認 |
| Dateが文字列になった | JSON経由で複製した | 型を維持する必要があるか検討 |
| 大きなデータで遅い | 深いコピーに時間とメモリーが必要 | 複製範囲を小さくする |
structuredClone() は万能なオブジェクト複製ではありません。関数やDOM要素は複製できず、独自クラスのプロトタイプやプロパティ記述子もそのまま保持されません。巨大なデータのコピーはメモリー消費に注意が必要です。移譲(transfer)の仕組みもありますが、元のバッファが利用できなくなる場合があるため、この入門教材では扱いません。
仕事で使うなら:変更前・変更後の比較
例えば業務システムの設定画面で「保存前のプレビュー」を作る場合、表示用の変更が元データに反映されると、キャンセルしても元に戻せないような不具合につながります。元の値を残したい範囲を確認し、データ型と容量に合うコピー方法を選びましょう。
この教材は架空データだけを使います。実際の顧客データや機密情報をデモに入力する必要はありません。画面のUI表示、通信、保存、WordPress上での表示は今回検証していません。
公式資料
MDN:Window.structuredClone() — 深いコピー、循環参照、例外とtransfer。
MDN:Structured clone algorithm — 複製可能な型と非対応の値。
MDN:Spread syntax — 浅いコピーの性質。
MDN:JSON.stringify() — JSON変換の制限。
図で整理する:コピー方法を選ぶ前に何を確認する?
コピーして変更した値が元データへ伝わるとき、原因は必ずしもJavaScriptのバグではありません。オブジェクトの参照関係を理解しないまま「コピーした」という言葉を使うと、外側の箱だけを複製したのか、中に入った箱も複製したのかが曖昧になります。浅いコピーは外側のオブジェクトを新しく作りますが、入れ子のオブジェクトは元と同じ参照を持つ場合があります。
flowchart TD
A["コピーする値を調べる"] --> B{"入れ子の値があるか"}
B -->|"ない"| C["浅いコピーでも足りるか検討"]
B -->|"ある"| D{"入れ子も独立させる必要があるか"}
D -->|"ない"| E["参照の共有を許可する設計か確認"]
D -->|"ある"| F["structuredClone対応の型か確認"]
F -->|"対応"| G["深いコピー後の変更を比較"]
F -->|"非対応"| H["用途に合わせ別の明示的な複製処理"]
G --> I["メモリー・性能・例外も確認"]
この図はあらゆるJavaScriptの値に使える万能な判定アルゴリズムではありません。たとえば関数、DOM要素、独自クラスのインスタンスなどは、そのままstructuredCloneで同じ機能を複製できるとは限りません。必要なのは「何を変えたいのか」「何を元の状態で残したいのか」を先に定めることです。
同じ設定を参照していることを確認する
次の二つの値が似ているからといって、二つの独立した設定が保存されているとは限りません。浅いコピーでは、外側のオブジェクトを作り直していても入れ子が同じ参照を指していることがあります。架空の色設定を用いた小さな例では、内側の参照が共有されているかを比較できます。
const original = { settings: { theme: "blue" } };
const shallow = { ...original };
console.log(original === shallow); // false と予想
console.log(original.settings === shallow.settings); // true と予想
外側は別でも、入れ子のsettingsは同じオブジェクトであることが分かります。したがってshallow.settings.themeを変更するとoriginal.settings.themeも変わるのです。逆に、shallow.settingsを新しいオブジェクトへ置き換えた場合は、元のsettingsそのものを書き換えたことにはなりません。「浅いコピーだから何を変えても必ず元へ伝わる」と覚えるのも誤りです。
深いコピーの前後で何を比較する?
深いコピーが必要な場面は、単に「入れ子がある」場面ではありません。元の設定と編集用の設定を独立させたい場合に意味があります。たとえば業務画面に「変更をプレビューする」「キャンセルする」という機能があるとします。プレビューで元の設定が直接変更されると、キャンセルしても以前の値に戻せない不具合を生みます。だから編集用の値を独立させる必要があります。
一方、大きなデータを毎回丸ごと複製すると、メモリー消費や処理時間が問題になることがあります。必要な範囲が一部分だけなら、その部分だけを明示的に更新する設計も比較対象になります。コピー方法を選ぶときは、正しさだけでなく変更範囲とデータ量を考えます。
JSON経由のコピーが万能でない理由
JSON.parse(JSON.stringify(value))という方法を見かけることがありますが、Dateは文字列になるなど元の型と同じにならない場合があります。Mapや関数、循環参照などを含む値では、表現できないものや期待した結果にならないものがあります。「二段階で書けるから同じ深いコピー」と考えず、コピーするデータがどんな型を持つかを確認します。
structuredCloneにも複製できない値があり、DataCloneErrorになる場合があります。独自クラスの振る舞い、getter、プロパティ記述子まで完全にそのまま保存されるとは限りません。例外が出たときに元データまで中途半端に変更されないよう、コピー処理と編集処理を分けて設計することが重要です。
業務で役に立つ「キャンセルできる変更」の考え方
架空の申請画面では、保存前に設定を変更して確認し、気に入らなければキャンセルすることがあります。このとき、画面の変更は仮のデータにだけ適用し、保存が確定してから本来のデータへ反映するのが自然です。コピー方式はそのための一つの道具ですが、保存先の状態をどう更新するか、複数利用者が同時に編集する場合にどう競合を検出するかは別の設計課題です。
この実験の見どころは、値が同じかどうかだけではありません。「どこが同じオブジェクトを参照しているか」を追うことです。コードを変更するたびに、外側の比較、入れ子の比較、変更後の元データをそれぞれ観察すると、なぜ予想と違う値になったのか説明できるようになります。単に新しいAPIを覚える以上に、変更がどこへ伝わるか考える訓練になります。
まとめ
コピーは一種類ではありません。まずは 浅いコピー→元の入れ子が変わる→深いコピー→元が変わらない の順に観察してください。実務では、何を複製したいのか、どのデータ型が含まれるのかを確かめることが、安全な設計への近道です。
