オブジェクトのマージ(浅)— スプレッド構文 {…a, …b} の基本と実践
複数の設定やデータを“上書きルール”で一つにまとめたいときに使うのがスプレッド構文のマージ。右側が勝つ(後勝ち)という直感的な挙動で、初心者でも扱いやすいです。
基本の使い方と後勝ちルール
const a = { theme: "light", pageSize: 20 };
const b = { pageSize: 50, compact: true };
const merged = { ...a, ...b };
console.log(merged);
// { theme: "light", pageSize: 50, compact: true }
JavaScript- 後勝ち: 同じキーがある場合、右側(後ろ)の値で上書きされます。
- 新規キー: 片方にしかないキーはそのまま取り込まれます。
- 浅いマージ: 上位階層だけコピー。ネストは「参照ごと」上書きされます。
よく使うテンプレート集
1) デフォルト設定 + ユーザー設定(右側が優先)
const defaults = { theme: "light", pageSize: 20, compact: false };
const user = { pageSize: 50 };
const config = { ...defaults, ...user };
// { theme: "light", pageSize: 50, compact: false }
JavaScript2) ネストは丸ごと上書き(部分更新ではない)
const base = { ui: { theme: "light", fontSize: 14 } };
const patch = { ui: { fontSize: 16 } };
const merged = { ...base, ...patch };
// { ui: { fontSize: 16 } }(themeは消える=ネストは“入れ替え”)
JavaScript3) 部分的にネストを保ちながら更新(段階的スプレッド)
const base = { ui: { theme: "light", font: { size: 14, family: "Noto" } } };
const patch = { ui: { font: { size: 16 } } };
// uiまでは再構築し、fontはさらに深くマージ
const merged = {
...base,
ui: {
...base.ui,
font: { ...base.ui.font, ...patch.ui.font },
},
};
// { ui: { theme: "light", font: { size: 16, family: "Noto" } } }
JavaScript4) 配列は“結合”ではなく“上書き”
const a = { tags: ["js", "web"] };
const b = { tags: ["ui"] };
const merged = { ...a, ...b };
// { tags: ["ui"] }(配列は結合されない=右で上書き)
JavaScript- 結合したいなら
tags: [...a.tags, ...b.tags]のように明示します。
5) 条件付きマージ(存在するものだけ)
const base = { a: 1, b: 2 };
const maybe = null;
const merged = { ...base, ...(maybe ?? {}) };
// null/undefined のときは無視、オブジェクトなら展開
JavaScriptObject.assign との違いと使い分け
const a = { x: 1 }, b = { y: 2 };
// 新しいオブジェクトに集約(不変に優しい)
const m1 = { ...a, ...b };
// 既存オブジェクトへ代入(破壊的)
const target = {};
Object.assign(target, a, b); // target が更新される
JavaScript- どちらも浅いマージ: ネストは参照単位で上書き。
- 不変を保つなら: スプレッドで「新しいオブジェクト」を作るのが定石。
- 複数をまとめる: assignもスプレッドも任意の数のオブジェクトに使えます。
実務での便利パターン
安全な更新(状態管理に向く書き方)
// React/Vueなどの状態更新で
const next = { ...state, loading: true, error: null };
JavaScriptキーの追加・上書きを同時に
const user = { name: "Aki", role: "user" };
const patched = { ...user, role: "admin", lastLogin: Date.now() };
JavaScript許容キーのみをマージ(ホワイトリスト)
const payload = { id: 1, name: "Aki", debug: true };
const allowed = (({ id, name }) => ({ id, name }))(payload);
const merged = { ...defaults, ...allowed };
JavaScriptマージ結果を整形(配列だけ結合)
const a = { tags: ["js"], meta: { v: 1 } };
const b = { tags: ["web"], meta: { v: 2 } };
const merged = {
...a, ...b,
tags: [...(a.tags ?? []), ...(b.tags ?? [])], // 結合
};
JavaScript落とし穴と対策
- ネストが消える問題: 同じキーにオブジェクトが来ると“丸ごと上書き”される。
対策: 段階的に{...}を入れて必要な深さまで手でマージする。 - 配列は結合されない: 期待に反して右で上書き。
対策:[...]で明示的に結合する。 - 非列挙・Symbolキー: スプレッドは「列挙可能な自前プロパティ」を対象。非列挙は含まれない。
対策: 特殊なプロパティはObject.getOwnPropertyDescriptorsなどで扱う。 - getter/setterの挙動: スプレッドは「値」をコピー。プロパティディスクリプタ(getter/setter)は再現されない。
対策: ディスクリプタごと複製したい場合はObject.defineProperties系で移植。 - null/undefinedの展開:
...nullや...undefinedはエラー。
対策:...(obj ?? {})のようにガード。 - 深いマージが必要: ルール付きでオブジェクトを“結合”したいとき、スプレッドだけでは冗長。
対策: 仕様を関数化するか、必要に応じて専用の“深いマージ”手法(自作・ライブラリ)を検討。
練習問題(手を動かして覚える)
// 1) 後勝ちを確認
const a = { level: 1, hp: 100 };
const b = { hp: 80, mp: 50 };
console.log({ ...a, ...b }); // { level:1, hp:80, mp:50 }
// 2) ネストの部分更新(段階的スプレッド)
const base = { user: { name: "Aki", pref: { lang: "ja", tz: "Asia/Tokyo" } } };
const patch = { user: { pref: { lang: "en" } } };
const merged = {
...base,
user: { ...base.user, pref: { ...base.user.pref, ...patch.user.pref } },
};
console.log(merged.user.pref); // { lang:"en", tz:"Asia/Tokyo" }
// 3) 配列は結合したい
const p = { tags: ["a", "b"] };
const q = { tags: ["c"] };
console.log({ ...p, ...q, tags: [...p.tags, ...q.tags] }); // ["a","b","c"]
// 4) ガード付きの条件マージ
const base2 = { a: 1 };
const optional = Math.random() > 0.5 ? { b: 2 } : null;
const merged2 = { ...base2, ...(optional ?? {}) };
console.log(merged2);
JavaScript直感的な指針
- 浅いマージの定石:
{...left, ...right}(右が勝つ)。 - ネストを維持した更新: 必要な深さまで段階的に
{...}を噛ませる。 - 配列は上書きされる: 結合したいなら
[...]を明示。 - 不変更新を習慣化: 新しいオブジェクトを作って状態を安全に更新。
- 特殊プロパティや深い結合が必要なら: 仕様を関数化、または専用の深いマージ手段を選ぶ。
