JavaScript | 配列・オブジェクト:オブジェクト操作 – スプレッド構文によるコピー

JavaScript JavaScript
スポンサーリンク

スプレッド構文によるコピーとは何か

スプレッド構文 { ...obj } は「オブジェクトの“自分が持つ列挙可能なプロパティ”を、浅く(シャロー)コピーして新しいオブジェクトを作る」ための書き方です。ここが重要です:コピーされるのは“文字列キーと Symbol キーのうち enumerable: true”だけ。プロトタイプ上のプロパティは含まれません。ターゲット(左側)が新しいオブジェクトになるため、元を破壊せずに安全な更新が書けます。

const user = { id: 1, name: "Alice" };
const copy = { ...user };               // 浅いコピー
console.log(copy); // { id: 1, name: "Alice" }
console.log(copy === user); // false(別インスタンス)
JavaScript

浅いコピー(シャローコピー)の挙動を理解する

入れ子は“参照を共有”する

スプレッドは1階層分の値をコピーします。入れ子のオブジェクト・配列は参照がそのまま移るため、内側を書き換えると元にも影響します。

const orig = { profile: { name: "Alice" }, tags: ["js"] };
const shallow = { ...orig };           // 浅いコピー

shallow.profile.name = "Bob";
console.log(orig.profile.name);        // "Bob"(同じ参照)
JavaScript

ここが重要です:入れ子まで独立させたいなら“必要な階層でさらにスプレッドする”か、“深いコピー”を使います。

// 入れ子の一部分だけ非破壊更新
const updated = {
  ...orig,
  profile: { ...orig.profile, name: "Carol" },
  tags: [...orig.tags, "web"],
};
JavaScript

非破壊更新(イミュータブル更新)の定番パターン

外側のフィールドを置き換える

const user = { id: 1, name: "Alice", role: "user" };
const renamed = { ...user, name: "Bob" };        // name を上書き
JavaScript

右側が後勝ちです。重ねる順序で“どれが優先されるか”を明確にできます。

入れ子の一部だけ安全に更新する

const state = { prefs: { theme: "light", lang: "ja" }, user: { name: "Alice" } };
const next = {
  ...state,
  prefs: { ...state.prefs, theme: "dark" },      // 部分更新
};
JavaScript

ここが重要です:UI状態や設定では“元を直接変更”せず“新インスタンスを返す”のが鉄則。変更検知が安定し、バグも減ります。


Object.assign との違いと使い分け

記述の簡潔さと安全性

  • スプレッドは「常に新しいオブジェクト」を返すため、破壊的に書き換えません。読みやすく、ミスが減ります。
  • Object.assign は第1引数(ターゲット)を直接変更します。非破壊で使いたいなら Object.assign({}, a, b) と書く必要があります。
// 非破壊(スプレッド)
const merged1 = { ...a, ...b };

// 非破壊(assign)
const merged2 = Object.assign({}, a, b);

// 破壊的(assign)
Object.assign(a, b); // a が書き換わる
JavaScript

属性・アクセサの扱い(どちらも“値コピー”)

どちらも getter の“値”をコピーします。プロパティの属性(writable/configurable/enumerable)や getter/setter 自体は複製されません。

const src = { get price() { return 100; } };
const copy = { ...src };
console.log(copy.price); // 100(値としてコピー)
JavaScript

注意点(重要ポイントの深掘り)

non-enumerable はコピーされない

Object.defineProperty(obj, "x", { enumerable: false }) で作ったプロパティはスプレッド対象外です。必要なら Object.getOwnPropertyNamesdefineProperty を使って属性ごと扱います。

Symbol キーは enumerable: true ならコピーされる

const k = Symbol("id");
const obj = { [k]: 123, x: 1 };
const copy = { ...obj };
console.log(copy[k], copy.x); // 123, 1
JavaScript

プロトタイプは引き継がれない

スプレッドで作るのは“素のオブジェクト”。クラスインスタンスをスプレッドしても、メソッドやプロトタイプの振る舞いは“コピーされない”点に注意。

class User { constructor() { this.name = "A"; } greet() { return "hi"; } }
const u = new User();
const plain = { ...u }; // { name: "A" }(メソッドは含まれない)
JavaScript

配列でのスプレッド(併用テクニック)

配列の浅いコピーと結合

const arr = [1, 2];
const copy = [...arr];              // 浅いコピー
const merged = [...arr, 3, 4];      // 結合
JavaScript

入れ子の配列要素は参照共有です。要素のオブジェクトを独立させたい場合は、要素ごとにスプレッドします。

const list = [{ x: 1 }, { x: 2 }];
const safe = list.map(item => ({ ...item })); // 要素ごとに浅いコピー
JavaScript

深いコピーが必要なときの選択肢

structuredClone(推奨:対応環境で)

structuredClone(obj) は多くの型(Date/Map/Set/TypedArray など)に対応した“深いコピー”を作ります。循環参照も扱えます。

const deep = structuredClone(orig);
JavaScript

JSON 経由のコピー(制限あり)

JSON.parse(JSON.stringify(obj)) は関数・undefined・Symbol・循環参照を失います。純粋データ(数値・文字列・配列・素のオブジェクト)に限定される用途でのみ使いましょう。


実践レシピ(よく使う更新パターン)

既定値+ユーザー設定+一時上書き

const defaults = { theme: "light", lang: "ja", retry: 2 };
const userCfg = { retry: 5 };
const runtime = { theme: "dark" };
const cfg = { ...defaults, ...userCfg, ...runtime }; // 後勝ち
JavaScript

“フィールドを除外する”コピー

const user = { id: 1, name: "Alice", password: "secret" };
const { password, ...safe } = user;               // password を除外
JavaScript

条件付きでフィールドを追加

const addIf = (obj, cond, key, val) =>
  cond ? { ...obj, [key]: val } : obj;

const out = addIf({ a: 1 }, true, "b", 2); // { a:1, b:2 }
JavaScript

まとめ

スプレッド構文は「安全で読みやすい“浅いコピー”と非破壊更新」を短く書ける武器です。入れ子は参照共有になるため、更新したい階層でさらにスプレッドして独立させるのが基本。non-enumerable はコピーされず、getter/setter は値だけが移る。クラスの振る舞いは複製されない点に注意し、深いコピーが必要なら structuredClone を選ぶ。設定の合成、部分更新、除外コピー、条件付き追加などの定番パターンを習得すれば、初心者でも壊れにくく明快なオブジェクト操作を書けます。

タイトルとURLをコピーしました