---
type: how-to
language: javascript
slug: object/merge
title: "オブジェクトを結合する"
title_tag: "JavaScript オブジェクトの結合 — スプレッドと浅いコピー"
summary: >
  結合はスプレッド構文が最短です。ただしコピーされるのは1階層目だけで、
  入れ子は元と共有されます。undefined で上書きされる罠と、深く複製する方法までまとめます。
description: >
  スプレッド構文は1段しか複製しません。入れ子が共有される理由、structuredClone との違い、JSON を経由した複製で Date や undefined が失われる点まで実行して示します。
status: published
difficulty: 2
minutes: 9

versions:
  verified: "Node 22.22.3"
  since: "ES2018"
  deprecated: null
  removed: null

sources:
  - title: "Object.assign — ECMAScript® 2026 Language Specification"
    url: "https://tc39.es/ecma262/#sec-object.assign"
  - title: "structuredClone() — MDN"
    url: "https://developer.mozilla.org/ja/docs/Web/API/Window/structuredClone"
  - title: "スプレッド構文 — MDN"
    url: "https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax"

terms: [スプレッド構文, 浅いコピー, 深いコピー, 参照]

links:
  related:
    - javascript/how-to/array/remove-duplicates
    - javascript/errors/is-not-a-function
    - javascript/reference/object/keys
    - javascript/reference/json/parse
    - javascript/learn/object/basics
    - javascript/how-to/object/pick

content_updated_at: 2026-09-07
published_at: 2026-09-07
---

## 答え

スプレッド構文で並べます。**後ろが勝ちます。**

```js run
const base = { name: 'あかり', role: 'user' };
const patch = { role: 'admin' };

const merged = { ...base, ...patch };

console.log(merged);
console.log(base);
```
```output
{ name: 'あかり', role: 'admin' }
{ name: 'あかり', role: 'user' }
```

元のオブジェクトは変わりません。**新しいオブジェクトが作られます。**

上のコードは書き換えて実行できます。`{ ...patch, ...base }` と順序を逆にしてみてください。

## Object.assign との違い

`Object.assign()` は[bad:第1引数を書き換えます]。

```js bad
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);
```
```output
{ name: 'あかり', role: 'admin' }
{ name: 'あかり', role: 'admin' }
true
```

`base` まで変わりました。戻り値も[key:同じオブジェクト]です。

書き換えたくないなら、空のオブジェクトを先頭に置きます。

```js run
const base = { name: 'あかり', role: 'user' };
const patch = { role: 'admin' };

const merged = Object.assign({}, base, patch);

console.log(base);
console.log(merged === base);
```
```output
{ name: 'あかり', role: 'user' }
false
```

| | 元を書き換えるか | 書き方 |
|---|---|---|
| `{ ...a, ...b }` | [em:書き換えない] | 短い。**ふだんはこちら** |
| `Object.assign(a, b)` | [bad:a を書き換える] | 意図して書き換えるときだけ |
| `Object.assign({}, a, b)` | [em:書き換えない] | スプレッドと同じ結果 |

## undefined でも上書きされる

**値が `undefined` でも、キーがあれば上書きします。**

```js bad
const base = { name: 'あかり', role: 'user' };
const patch = { role: undefined };

console.log({ ...base, ...patch });
```
```output
{ name: 'あかり', role: undefined }
```

`role` が消えたのではなく、`undefined` で[bad:塗りつぶされました]。

フォームの入力をそのまま重ねると、未入力の項目が既定値を壊します。
**先に `undefined` を落としてください。**

`Object.entries()` と `Object.fromEntries()` の詳しい話は
[Object.keys() / values() / entries()](/ja/javascript/reference/object/keys/) にあります。

```js run
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) });
```
```output
{ name: 'ゆうと', role: 'user' }
```

[dim:null は「意味のある値」として扱う場面が多いので、上では落としていません。落とすかどうかは仕様次第です。]

## コピーされるのは1階層目だけ

これが[key:いちばん多い事故]です。入れ子のオブジェクトは**元と共有されます**（[type:浅いコピー]）。

```js bad
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);
```
```output
[ 'a', 'b', 'c' ]
[ 'a', 'b', 'c' ]
true
```

`copy` だけを触ったつもりが、`base` も変わりました。
**`tags` は同じ配列を[type:参照]しているからです。**

[num:1]階層目は別物になっています。そこだけ見て「複製できた」と思うと踏みます。

```js run
const base = { name: 'あかり', tags: ['a'] };
const copy = { ...base };

copy.name = 'ゆうと';

console.log(base.name, copy.name);
console.log(base.tags === copy.tags);
```
```output
あかり ゆうと
true
```

### 深く複製する

`structuredClone()` を使います。**入れ子も全部たどって複製します。**

```js run
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);
```
```output
[ 'a', 'b' ] { n: 1 }
[ 'a', 'b', 'c' ] { n: 99 }
```

**元に影響しません。** `Date` や `Map` も複製できます。

```js run
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);
```
```output
true true
true
false
```

**関数は複製できません。** 入っていると例外になります。

```js bad
console.log(structuredClone({ fn: () => 1 }));
```
```output
DataCloneError: () => 1 could not be cloned.
```

[dim:structuredClone は Node 17 / Chrome 98 / Safari 15.4 以降で使えます。]

### JSON を経由する方法は勧めない

古くからある書き方ですが、**落ちるものが多すぎます。**

```js bad
const src = {
  d: new Date('2026-09-07'),
  u: undefined,
  n: NaN,
  m: new Map([['k', 1]]),
};

console.log(JSON.parse(JSON.stringify(src)));
```
```output
{ d: '2026-09-07T00:00:00.000Z', n: null, m: {} }
```

`Date` が[bad:文字列]に、`NaN` が[bad:null]に、`Map` が[bad:空オブジェクト]になり、
`undefined` のキーは[bad:消えました]。

**`structuredClone()` を使ってください。**

## 入れ子を混ぜたいとき

スプレッドを重ねても、**入れ子は丸ごと差し替わります。**

```js bad
const base = { name: 'あかり', config: { theme: 'dark', size: 12 } };
const patch = { config: { size: 16 } };

console.log({ ...base, ...patch });
```
```output
{ name: 'あかり', config: { size: 16 } }
```

`theme` が[bad:消えました]。混ぜたいなら、その階層でもスプレッドします。

```js run
const base = { name: 'あかり', config: { theme: 'dark', size: 12 } };
const patch = { config: { size: 16 } };

console.log({
  ...base,
  ...patch,
  config: { ...base.config, ...patch.config },
});
```
```output
{ name: 'あかり', config: { theme: 'dark', size: 16 } }
```

階層が深いなら、**再帰で書きます。**

```js run
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);
```
```output
{ a: 1, nested: { x: 1, y: { z: 9 }, w: 2 } }
{ a: 1, nested: { x: 1, y: { z: 1 } } }
```

[dim:配列をどう扱うか（置き換えるか繋ぐか）は仕様によって変わります。上は置き換えています。]

## キーの順序

数字に見えるキーだけ[key:先に来ます]。並べた順にはなりません。

```js run
const obj = { b: 1, 2: 2, a: 3, 1: 4 };

console.log(Object.keys(obj));
console.log({ ...obj });
```
```output
[ '1', '2', 'b', 'a' ]
{ '1': 4, '2': 2, b: 1, a: 3 }
```

`1` と `2` が[num:1]文字目に来て、そのあと文字のキーが**書いた順**で並びます。
**順序に依存する処理を書かないでください。** 必要なら `Map` か配列を使います。

`JSON.parse(JSON.stringify(...))` で複製すると何が失われるかは、JSON のページに一覧があります。
→ [JSON.parse() と JSON.stringify()](/ja/javascript/reference/json/parse/)
