How to Use Object Assign in JavaScript Properly
Learn how Object.assign() copies properties between objects. Use it for merging configuration, cloning objects, and setting defaults, while avoiding the shallow copy trap.
Object.assign() in JavaScript copies properties from one or more source objects into a target object. It is the built-in way to merge objects, apply default values, or create a shallow clone.
The method mutates the target object and returns it. This is different from the spread operator, which creates a new object. Understanding this distinction helps you choose the right tool for each situation.
Basic Syntax and Return Value
The method takes a target object first, followed by any number of source objects. Every property from each source gets copied onto the target, in the order the sources are listed:
Object.assign(target, ...sources)| Parameter | Description |
|---|---|
target | The object to copy properties into. This object is mutated. |
sources | One or more objects whose properties are copied into target. |
| Returns | The target object after all sources have been merged in. |
Here is the simplest case:
const target = { a: 1 };
const source = { b: 2 };
const result = Object.assign(target, source);
console.log(target); // { a: 1, b: 2 } -- mutated
console.log(result); // { a: 1, b: 2 } -- same reference as target
console.log(result === target); // trueThe return value is the same object as target. This is useful for chaining or passing the merged result directly to another function.
Merging Multiple Objects
You can pass more than one source object. Later sources overwrite properties from earlier sources when keys collide:
const defaults = { theme: "light", fontSize: 14, showSidebar: true };
const userPrefs = { theme: "dark", fontSize: 16 };
const sessionPrefs = { fontSize: 18 };
const finalConfig = Object.assign(defaults, userPrefs, sessionPrefs);
console.log(finalConfig);
// { theme: "dark", fontSize: 18, showSidebar: true }The order matters. Each later source overwrites matching keys from the sources before it, so sessionPrefs has the final say and defaults is overwritten first. A property that only exists in one object, like showSidebar, is kept untouched.
This pattern is common for merging configuration or applying cascading user preferences.
Cloning a Flat Object
Object.assign() can create a shallow copy by passing an empty object as the target:
const original = { x: 10, y: 20, label: "point" };
const clone = Object.assign({}, original);
console.log(clone); // { x: 10, y: 20, label: "point" }
console.log(clone === original); // false (different references)
// Changing the clone does not affect the original for top-level properties
clone.x = 99;
console.log(original.x); // 10 (unchanged)This works well for flat objects where every value is a primitive (string, number, boolean, null, undefined). But be careful with nested objects.
The Shallow Copy Trap
Object.assign() copies property values as-is. When a property value is an object or array, only the reference is copied:
const original = {
name: "Project Alpha",
metadata: { created: "2026-01-10", version: 2 }
};
const clone = Object.assign({}, original);
clone.name = "Project Beta";
console.log(original.name); // "Project Alpha" (unchanged)Changing a top-level primitive on the clone leaves the original untouched, because primitives are copied by value. Nested objects behave differently, since only the reference to them gets copied:
clone.metadata.version = 3;
console.log(original.metadata.version); // 3 (changed in both!)Both original and clone now point to the same metadata object. Changing it through one reference affects the other.
For deep cloning, where nested objects are recursively copied, use structuredClone() (available in modern browsers and Node.js 17+):
const deepClone = structuredClone(original);
deepClone.metadata.version = 99;
console.log(original.metadata.version); // 2 (safe)
console.log(deepClone.metadata.version); // 99For more cloning techniques, see the article on /javascript/how-to-clone-a-javascript-object-without-errors.
Applying Defaults Without Overwriting Existing Values
Sometimes you want to fill in missing properties without changing the ones that already exist. You can do this by putting the defaults first and the existing values last:
const userProvided = { name: "Sam", email: "sam@example.com" };
const defaults = { name: "Guest", email: "", role: "user" };
const merged = Object.assign({}, defaults, userProvided);
console.log(merged);
// { name: "Sam", email: "sam@example.com", role: "user" }The name and email fields come from userProvided, since it is listed last and its values overwrite the defaults. The role field comes from defaults because the user did not provide one.
If you reverse the order, the defaults would overwrite the user's values, which is not what you want.
Object.assign() vs the Spread Operator
The spread operator ... is a newer, often cleaner alternative to Object.assign(). Both produce the same merged result for a fresh object:
| Object.assign() | Spread ({ ...a, ...b }) | |
|---|---|---|
| Creates a new object | Only if target is an empty object | Always |
| Can mutate an existing object | Yes | No |
| Typical use | Merging into an existing target | Building a brand new merged object |
const a = { x: 1, y: 2 };
const b = { z: 3 };
console.log(Object.assign({}, a, b)); // { x: 1, y: 2, z: 3 }
console.log({ ...a, ...b }); // { x: 1, y: 2, z: 3 }The spread operator always creates a new object. Object.assign() can merge into an existing object. Use spread when you want a fresh object, and Object.assign() when you need to mutate an existing one:
// Mutate an existing object
Object.assign(existingConfig, newSettings);
// Create a new object (spread is cleaner)
const newConfig = { ...existingConfig, ...newSettings };For most everyday merges, the spread operator is the more readable choice. For more on spread syntax, see the article on /javascript/js-spread-operator-for-arrays-complete-tutorial.
Common Mistakes
Mutating the target by accident. If you pass an object you did not mean to change as the first argument, it gets modified:
const defaults = { timeout: 3000 };
const options = { timeout: 5000 };
Object.assign(defaults, options);
console.log(defaults.timeout); // 5000 -- defaults was mutatedTo avoid this, pass an empty object first: Object.assign({}, defaults, options).
Expecting a deep copy. Object.assign() only does a shallow copy. Nested objects and arrays are shared between the original and the result. Use structuredClone() for deep copies when you need isolation.
Forgetting that null and undefined sources are skipped. You can safely pass them without errors:
const result = Object.assign({}, { a: 1 }, null, undefined, { b: 2 });
console.log(result); // { a: 1, b: 2 }Overwriting with the wrong property order. If you put defaults last, they overwrite the values you actually wanted to keep, which quietly discards whatever the user or caller provided:
// Wrong: defaults overwrite the user's values
Object.assign({}, userPrefs, defaults);
// Correct: user values overwrite defaults
Object.assign({}, defaults, userPrefs);Rune AI
Key Insights
- Object.assign(target, ...sources) copies own enumerable properties from sources into target.
- It mutates the target object and returns it.
- Later sources overwrite properties from earlier sources with the same key.
- It performs a shallow copy: nested objects are shared, not cloned.
- For immutable merges that create a new object, use the spread operator instead: { ...a, ...b }.
Frequently Asked Questions
Does Object.assign() create a deep copy?
Should I use Object.assign() or the spread operator?
What happens if two source objects have the same property?
Conclusion
Object.assign() is a workhorse for copying properties from one or more source objects into a target object. Use it when you need to merge configurations, apply defaults, or clone flat objects. Remember that it only does a shallow copy, so nested objects and arrays are shared by reference. For most one-shot merges where you create a new object, the spread operator is the cleaner choice.
More in this topic
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.
How to Loop Through Arrays Using JS for Loops Guide
Looping through arrays is the most practical use of JavaScript for loops. Learn how to access each element by index, avoid off-by-one errors, and choose between for, for...of, and forEach.