CSのTips一覧

コピーしたつもりでも、ネストしたオブジェクトは同じものを指すことがある

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 / MDN

    スプレッド構文のコピーは1階層で、配列要素などの同一性を保つと説明している。structuredCloneも対応する型に範囲がある。

    資料を読む
  • Python copy

    copy.copyとcopy.deepcopyを区別し、浅いコピーが元の内側のオブジェクトへの参照を使うことを説明している。

    資料を読む

解説

編集フォーム、状態更新、テストデータの使い回しで使う。キャンセルしたのに元データが変わる場合は、変更する内側の参照を共有していないか確認する。

参照は、オブジェクトを指す手がかり。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は?

  1. "A"のまま
  2. 必ず例外になる
  3. "B"になる
解答と解説

"B"になる

外側だけのコピーなのでprofileは共有され、aからもBが見える。Aを保つには変更するprofileも別にする必要がある。この通常の変更可能なオブジェクトへの代入は例外にはならない。