結合はスプレッド構文が最短です。ただしコピーされるのは1階層目だけで、入れ子は元と共有されます。undefined で上書きされる罠と、深く複製する方法までまとめます。
答え
スプレッド構文で並べます。後ろが勝ちます。
example.js
const base = { name: 'あかり', role: 'user' };
const patch = { role: 'admin' };
const merged = { ...base, ...patch };
console.log(merged);
console.log(base);
出力書き換えて実行できます
{ name: 'あかり', role: 'admin' }
{ name: 'あかり', role: 'user' }
javascript
const base = { name: 'あかり', role: 'user' };
const patch = { role: 'admin' };
const merged = Object.assign(base, patch);
console.log(merged);
console.log(base);
console.log(merged === base);
{ name: 'あかり', role: 'admin' }
{ name: 'あかり', role: 'admin' }
true
元のオブジェクトは変わりません。新しいオブジェクトが作られます。
上のコードは書き換えて実行できます。{ ...patch, ...base } と順序を逆にしてみてください。
Object.assign との違い
Object.assign() は第1引数を書き換えます。
example.js
const base = { name: 'あかり', role: 'user' };
const patch = { role: 'admin' };
const merged = Object.assign({}, base, patch);
console.log(base);
console.log(merged === base);
出力書き換えて実行できます
{ name: 'あかり', role: 'user' }
false
javascript
const base = { name: 'あかり', role: 'user' };
const patch = { role: undefined };
console.log({ ...base, ...patch });
{ name: 'あかり', role: undefined }
base まで変わりました。戻り値も同じオブジェクトです。
書き換えたくないなら、空のオブジェクトを先頭に置きます。
example.js
function compact(obj) {
return Object.fromEntries(
Object.entries(obj).filter(([, v]) => v !== undefined),
);
}
const base = { name: 'あかり', role: 'user' };
const patch = { role: undefined, name: 'ゆうと' };
console.log({ ...base, ...compact(patch) });
出力書き換えて実行できます
{ name: 'ゆうと', role: 'user' }
javascript
const base = { name: 'あかり', tags: ['a', 'b'] };
const copy = { ...base };
copy.tags.push('c');
console.log(copy.tags);
console.log(base.tags);
console.log(copy.tags === base.tags);
[ 'a', 'b', 'c' ]
[ 'a', 'b', 'c' ]
true
|
元を書き換えるか |
書き方 |
{ ...a, ...b } |
書き換えない |
短い。ふだんはこちら |
Object.assign(a, b) |
a を書き換える |
意図して書き換えるときだけ |
Object.assign({}, a, b) |
書き換えない |
スプレッドと同じ結果 |
undefined でも上書きされる
値が undefined でも、キーがあれば上書きします。
example.js
const base = { name: 'あかり', tags: ['a'] };
const copy = { ...base };
copy.name = 'ゆうと';
console.log(base.name, copy.name);
console.log(base.tags === copy.tags);
出力書き換えて実行できます
あかり ゆうと
true
example.js
const base = { name: 'あかり', tags: ['a', 'b'], nested: { n: 1 } };
const copy = structuredClone(base);
copy.tags.push('c');
copy.nested.n = 99;
console.log(base.tags, base.nested);
console.log(copy.tags, copy.nested);
出力書き換えて実行できます
[ 'a', 'b' ] { n: 1 }
[ 'a', 'b', 'c' ] { n: 99 }
role が消えたのではなく、undefined で塗りつぶされました。
フォームの入力をそのまま重ねると、未入力の項目が既定値を壊します。
先に undefined を落としてください。
Object.entries() と Object.fromEntries() の詳しい話は
Object.keys() / values() / entries() にあります。
example.js
const src = { d: new Date('2026-09-07'), m: new Map([['k', 1]]) };
const copy = structuredClone(src);
console.log(copy.d instanceof Date, copy.m instanceof Map);
console.log(copy.d.getTime() === src.d.getTime());
console.log(copy.m === src.m);
出力書き換えて実行できます
true true
true
false
javascript
console.log(structuredClone({ fn: () => 1 }));
DataCloneError: () => 1 could not be cloned.
null は「意味のある値」として扱う場面が多いので、上では落としていません。落とすかどうかは仕様次第です。
コピーされるのは1階層目だけ
これがいちばん多い事故です。入れ子のオブジェクトは元と共有されます(浅いコピー)。
javascript
const src = {
d: new Date('2026-09-07'),
u: undefined,
n: NaN,
m: new Map([['k', 1]]),
};
console.log(JSON.parse(JSON.stringify(src)));
{ d: '2026-09-07T00:00:00.000Z', n: null, m: {} }
javascript
const base = { name: 'あかり', config: { theme: 'dark', size: 12 } };
const patch = { config: { size: 16 } };
console.log({ ...base, ...patch });
{ name: 'あかり', config: { size: 16 } }
copy だけを触ったつもりが、base も変わりました。
tags は同じ配列を参照しているからです。
1階層目は別物になっています。そこだけ見て「複製できた」と思うと踏みます。
example.js
const base = { name: 'あかり', config: { theme: 'dark', size: 12 } };
const patch = { config: { size: 16 } };
console.log({
...base,
...patch,
config: { ...base.config, ...patch.config },
});
出力書き換えて実行できます
{ name: 'あかり', config: { theme: 'dark', size: 16 } }
example.js
function isPlainObject(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
function deepMerge(a, b) {
const out = { ...a };
for (const [k, v] of Object.entries(b)) {
out[k] = isPlainObject(v) && isPlainObject(a[k]) ? deepMerge(a[k], v) : v;
}
return out;
}
const base = { a: 1, nested: { x: 1, y: { z: 1 } } };
const patch = { nested: { y: { z: 9 }, w: 2 } };
console.log(deepMerge(base, patch));
console.log(base);
出力書き換えて実行できます
{ a: 1, nested: { x: 1, y: { z: 9 }, w: 2 } }
{ a: 1, nested: { x: 1, y: { z: 1 } } }
深く複製する
structuredClone() を使います。入れ子も全部たどって複製します。
example.js
const obj = { b: 1, 2: 2, a: 3, 1: 4 };
console.log(Object.keys(obj));
console.log({ ...obj });
出力書き換えて実行できます
[ '1', '2', 'b', 'a' ]
{ '1': 4, '2': 2, b: 1, a: 3 }
元に影響しません。 Date や Map も複製できます。
関数は複製できません。 入っていると例外になります。
structuredClone は Node 17 / Chrome 98 / Safari 15.4 以降で使えます。
JSON を経由する方法は勧めない
古くからある書き方ですが、落ちるものが多すぎます。
Date が文字列に、NaN がnullに、Map が空オブジェクトになり、
undefined のキーは消えました。
structuredClone() を使ってください。
入れ子を混ぜたいとき
スプレッドを重ねても、入れ子は丸ごと差し替わります。
theme が消えました。混ぜたいなら、その階層でもスプレッドします。
階層が深いなら、再帰で書きます。
配列をどう扱うか(置き換えるか繋ぐか)は仕様によって変わります。上は置き換えています。
キーの順序
数字に見えるキーだけ先に来ます。並べた順にはなりません。
1 と 2 が1文字目に来て、そのあと文字のキーが書いた順で並びます。
順序に依存する処理を書かないでください。 必要なら Map か配列を使います。
JSON.parse(JSON.stringify(...)) で複製すると何が失われるかは、JSON のページに一覧があります。
→ JSON.parse() と JSON.stringify()