Object.assign() was the original way to merge objects in JavaScript, predating the object spread operator. Professional codebases still encounter it constantly in older code and in a few situations spread cannot cover as cleanly.
1Object.assign() | JavaScript Tutorial - In-Depth Guide Part 1
Object.assign(target, ...sources) copies own enumerable properties from one or more source objects into a target object, returning the target.
const target = { a: 1 };
Object.assign(target, { b: 2 });
target; // { a: 1, b: 2 } โ target was mutated!Merging into Target
2Object.assign() | JavaScript Tutorial - In-Depth Guide Part 2
To avoid mutating an existing object, pass an empty object literal as the first argument โ this is the classic pre-spread idiom for a non-mutating merge.
const merged = Object.assign({}, defaults, overrides);
// equivalent to: { ...defaults, ...overrides }Non-Mutating Idiom
3Object.assign() | JavaScript Tutorial - In-Depth Guide Part 3
Like spread, Object.assign() only performs a shallow copy and follows the same left-to-right, later-source-wins overwrite rule.
Object.assign({}, { x: 1 }, { x: 2 }); // { x: 2 }Same Shallow Semantics
4Object.assign() | JavaScript Tutorial - In-Depth Guide Part 4
Object.assign() copies a getter's current computed value, not the getter definition itself โ the target ends up with a plain data property.
const source = { get random() { return Math.random(); } };
const copy = Object.assign({}, source);
// copy.random is a fixed number, not a live getterGetters Become Values
5Object.assign() | JavaScript Tutorial - In-Depth Guide Part 5
Object.assign() is still preferred over spread in a few cases: merging into an existing object on purpose, or merging class instances where spread would lose the prototype.
class Point { distance() { /* ... */ } }
const p = Object.assign(new Point(), { x: 1, y: 2 });
p.distance(); // still works, p is still a PointWhen Assign Still Wins
6Step-by-Step Breakdown
Object.assign(target, ...sources) copies own enumerable properties from one or more source objects into a target object, returning the target.
Checkpoint: Does Object.assign(target, source) mutate the target object?
- โYes, target is mutated and also returned
- โNo, it always returns a brand-new object
To avoid mutating an existing object, pass an empty object literal as the first argument โ this is the classic pre-spread idiom for a non-mutating merge.
Like spread, Object.assign() only performs a shallow copy and follows the same left-to-right, later-source-wins overwrite rule.
Object.assign() copies a getter's current computed value, not the getter definition itself โ the target ends up with a plain data property.
Object.assign() is still preferred over spread in a few cases: merging into an existing object on purpose, or merging class instances where spread would lose the prototype.
Checkpoint: Does spreading a class instance ({ ...instance }) preserve its prototype and methods?
- โYes, spread preserves the prototype
- โNo, it produces a plain object without the prototype
Next, we'll explore 'Deep Copy vs Shallow Copy'.
Level Up ๐
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Be Careful Merging Accessibility Props with Object.assign()
Using Object.assign(defaultProps, userProps) without a fresh target can mutate a shared defaultProps object used by other component instances, silently corrupting their default aria attributes too.
SEO Implications
- 1
No Direct SEO Effect
Object.assign() is a data-manipulation utility; SEO relevance is limited to indirect effects from bugs caused by unintended shared-object mutation.
Best Practices
Prefer Spread for Simple, Non-Mutating Merges
Object spread is more concise and its non-mutating behavior is the default, avoiding the target-mutation footgun that Object.assign() carries.
Reach for Object.assign() When You Specifically Need to Mutate an Existing Object
When the intent genuinely is "add these properties onto this existing object" (e.g. initializing an instance), Object.assign() communicates that intent more directly than a spread-and-reassign.
Frequent Bugs
Calling `Object.assign(defaults, overrides)` intending to create a merged copy, but accidentally mutating the shared `defaults` object referenced elsewhere in the app.
Always pass a fresh empty object literal as the first argument โ `Object.assign({}, defaults, overrides)` โ when you want a non-mutating merge.
Expecting a getter copied via Object.assign() to remain dynamically computed on the copy, then being confused when it holds a stale, frozen-in-time value.
Remember assign() copies the current value of a getter, not its definition; if a live computed property is needed on the copy, redefine it explicitly with Object.defineProperty.
Real-World Examples
Initializing a Class Instance from a Plain Data Object
A data layer needed to hydrate class instances (with methods) from plain JSON objects returned by an API, without losing the class's prototype.
class User {
greet() { return `Hi, ${this.name}`; }
}
const user = Object.assign(new User(), jsonFromApi);
user.greet(); // works, user is still a real User instance