コピーしたつもりでも、ネストしたオブジェクトは同じものを指すことがある
JavaScriptのスプレッド構文やPythonの浅いコピーは、外側を新しくしても内側のオブジェクトへの参照を共有する。どの階層を変更するかに合わせてコピーする。
用語
- 参照
- オブジェクトそのものを複製せず、同じオブジェクトを指すための手がかり。
- 浅いコピー
- 外側の入れ物を新しくし、内側のオブジェクトへの参照は共有するコピー。
- 深いコピー
- 内側のオブジェクトも再帰的に複製するコピー。対応する型や共有関係の扱いは方式による。
図と例
originalをスプレッドでコピー → originalとdraftは別オブジェクト → 両方のaddressは同じオブジェクト → draft.addressの変更がoriginalにも見える
コード例:const original = {address: {city: "Tokyo"}}; const draft = {...original}; draft.address.city = "Osaka"; のあと、original.address.cityもOsaka。次に const next = {...original, address: {...original.address, city: "Kyoto"}}; と変更するaddressも作り直せば、original.address.cityはOsakaのまま、next.address.cityだけKyotoになる。
実際のサービス・製品では
解説
編集フォーム、状態更新、テストデータの使い回しで使う。キャンセルしたのに元データが変わる場合は、変更する内側の参照を共有していないか確認する。
参照は、オブジェクトを指す手がかり。JavaScriptでconst draft = {...original}とすると外側のオブジェクトは別になるが、original.addressがオブジェクトならdraft.addressと同じものを指す。この浅いコピーのあとにdraft.address.cityを書き換えると、original.address.cityも変わる。constは変数の再代入を禁止するだけで、オブジェクト内部を変更不可にはしない。変更する階層まで新しいオブジェクトを作るか、対応するデータ型なら深いコピーを検討する。すべてを深くコピーすると、費用が増えたり、意図した共有を壊したりする場合もある。
トレードオフ
- 変更する階層だけ作り直す
- 得るもの:変更しない部分の共有を保ちつつ元を守れる
- 払うもの:変更する経路の各階層を忘れずに扱う必要がある
- 対応する型のデータを深くコピーする
- 得るもの:複数の内側の値を編集する下書きを分けやすい
- 払うもの:コピーの費用と型の制約、意図した共有への影響を確認する必要がある
さらに学ぶ
チェック
JavaScriptでa = {profile: {name: "A"}}、b = {...a}とし、b.profile.name = "B"を実行した。a.profile.nameは?
- "A"のまま
- 必ず例外になる
- "B"になる
解答と解説
"B"になる
外側だけのコピーなのでprofileは共有され、aからもBが見える。Aを保つには変更するprofileも別にする必要がある。この通常の変更可能なオブジェクトへの代入は例外にはならない。