Object.assign()

Merging, and the source of two persistent misunderstandings: the first argument is changed, and nested objects are shared rather than copied.

Object static methodES2015Live demo
Common call
Object.assign({}, defaults, options)
Returns
the target, mutated
Replaces
a manual property-copying loop
Watch out
pass {} as the target unless you MEAN to mutate
Object.assign(targettarget — The object that receives the properties and is MODIFIED. It is also what gets returned.type: object · required, ...sources...sources — Objects to copy from, left to right — later sources win. Only own enumerable properties are copied, including symbol keys. null and undefined sources are skipped silently.type: object · default: none)
→ object

Demo

Live evaluation
Try:
Inputs
astringtarget JSON, e.g. {"a":1}
bstringsource JSON, e.g. {"b":2}
Output
Object.assign(JSON.parse('{"a":1}'), JSON.parse('{"b":2}'))
{ a: 1, b: 2 }

Disjoint keys merge and a repeated key takes the source value — straightforward. The third case is the one that catches people: assign is SHALLOW, so the nested object is not merged property by property. The whole of n is replaced by the source version, and the y that existed only in the target is gone. The last case shows there is no special treatment for null values either — null is a value like any other and overwrites what was there, which is why assign is a poor fit for options merging where absent means keep the default.

Parameters

NameTypeRequiredDescription
targetobjectyesThe object that receives the properties and is MODIFIED. It is also what gets returned.
...sourcesobjectno (none)Objects to copy from, left to right — later sources win. Only own enumerable properties are copied, including symbol keys. null and undefined sources are skipped silently.

Return value

object — The TARGET object itself, modified in place — not a new object. Later sources overwrite earlier ones.

Common patterns

Merge into a fresh object
The empty target is what keeps the inputs untouched.
const config = Object.assign({}, defaults, options);
Spread is usually clearer
Same semantics, no mutable target to get wrong.
const config = {...defaults, ...options};
Copy while keeping the prototype
Where spread would flatten to a plain object.
Object.assign(Object.create(Object.getPrototypeOf(src)), src);

Examples

1. Merge
Object.assign({a: 1}, {b: 2})
Returns
{a: 1, b: 2}
2. Source wins
Object.assign({a: 1}, {a: 9})
Returns
{a: 9}
3. Target is mutated
const t = {a: 1}; Object.assign(t, {b: 2}); t
Returns
{a: 1, b: 2}
4. It returns the target
const t = {}; Object.assign(t, {a: 1}) === t
Returns
true
5. Shallow
const s = {n: {x: 1}}; const c = Object.assign({}, s); c.n === s.n
Returns
true
6. Inherited skipped
const o = Object.create({x: 1}); o.y = 2; Object.assign({}, o)
Returns
{y: 2}

Pitfalls

1. It mutates the first argument
The single most common misuse. Object.assign(defaults, options) modifies your defaults object permanently, so every later use of it is contaminated — and because the return value looks right, the bug shows up somewhere else entirely.
Defaults destroyed
const d = {a: 1};
Object.assign(d, {a: 9});
d
{a: 9}
Fresh target
const d = {a: 1};
const merged = Object.assign({}, d, {a: 9});
d
{a: 1}
2. The copy is shallow
Nested objects are copied by reference, so the "copy" shares them with the original. Mutating a nested property through either one changes both, which defeats the entire purpose of having cloned.
Shared nesting
const s = {n: {x: 1}};
const c = Object.assign({}, s);
c.n.x = 9;
s.n.x
9
Deep clone
const c = structuredClone(s);
c.n.x = 9;
s.n.x
1
3. It triggers setters on the target
Properties are ASSIGNED, not defined, so a setter on the target runs and a read-only property throws in strict mode. Copying descriptors requires getOwnPropertyDescriptors with defineProperties instead.
Setter runs
const t = {set a(v) { throw new Error("no"); }};
Object.assign(t, {a: 1})
Error: no
Define instead
Object.defineProperties(t, Object.getOwnPropertyDescriptors(src))
no setter invoked
4. Getters are evaluated, not copied
A source getter is invoked once and its RESULT is stored as a plain value. The copy therefore freezes what was a live computed property, and it will never update again.
Snapshot
const s = {get now() { return Date.now(); }};
Object.assign({}, s).now
a fixed number
Copy the descriptor
Object.defineProperties({}, Object.getOwnPropertyDescriptors(s))
still a getter

When to use

Use it
  • Merging several sources into one new object
  • Deliberately updating an existing object in place
  • Copying while preserving a prototype, with Object.create
  • Copying symbol-keyed properties, which JSON round trips lose
Reach for something else
  • You just want a merged literal → spread, which cannot mutate by accident
  • Nested data must be independent → structuredClone
  • Absent should mean "keep the default" → filter undefined first
  • You need getters and setters preserved → getOwnPropertyDescriptors

Notes

Complexity
O(n) in the total number of own enumerable properties across sources
Return
The target object, mutated — never a new object
CPython impl
V8: Builtins-object-assign
Memory
No new object unless you pass a fresh target
Thread-safe
Single-threaded; sources are read, target is written

FAQ

Spread for building a new object — {...a, ...b} cannot accidentally mutate anything and reads better. Object.assign when you genuinely want to write into an existing object, or when you need symbol keys and a preserved prototype, which object spread also handles for symbols but flattens the prototype.

const merged = {...defaults, ...options};   // new object
Object.assign(existing, patch);             // deliberate mutation

History

ES2015
Object.assign added, standardising the extend function every library shipped.
ES2018
Object spread syntax arrived and became the idiomatic form for building new objects.