How to Use Object.assign in JavaScript Properly
Master Object.assign in JavaScript for merging objects, copying properties, creating defaults, and cloning. Learn shallow copy behavior, common pitfalls, spread operator comparison, and real-world patterns.
Object.assign() is a built-in JavaScript method that copies properties from one or more source objects into a target object. It is used for merging objects, applying default settings, cloning data, and composing configurations. While the spread operator has largely replaced Object.assign for simple merges, Object.assign remains essential for mutating existing objects and for environments or patterns where spread syntax is not an option.
This guide covers every aspect of Object.assign, from basic syntax to shallow copy behavior, real-world patterns, and the differences between Object.assign and spread.
Basic Syntax
Object.assign(target, ...sources)- target: The object to copy properties into (gets modified)
- sources: One or more objects to copy properties from
- Returns: The target object
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = Object.assign(target, source);
console.log(result); // { a: 1, b: 3, c: 4 }
console.log(target); // { a: 1, b: 3, c: 4 } — target IS modified
console.log(result === target); // true — returns the same objectMerging Multiple Objects
Object.assign accepts multiple source objects. Properties from later sources overwrite earlier ones:
const defaults = { theme: "light", fontSize: 14, language: "en" };
const userPrefs = { theme: "dark", fontSize: 18 };
const sessionOverrides = { fontSize: 20 };
const settings = Object.assign({}, defaults, userPrefs, sessionOverrides);
console.log(settings);
// { theme: "dark", fontSize: 20, language: "en" }The priority order is left to right: defaults < userPrefs < sessionOverrides.
Merge Behavior Visualization
const a = { x: 1, y: 2 };
const b = { y: 3, z: 4 };
const c = { z: 5, w: 6 };
const merged = Object.assign({}, a, b, c);
// Step 1: {} + a → { x: 1, y: 2 }
// Step 2: + b → { x: 1, y: 3, z: 4 } (y overwritten)
// Step 3: + c → { x: 1, y: 3, z: 5, w: 6 } (z overwritten)
console.log(merged); // { x: 1, y: 3, z: 5, w: 6 }Creating Object Copies
Shallow Clone
Use an empty object {} as the target to create a copy without mutating the original:
const original = {
name: "Alice",
age: 28,
hobbies: ["reading", "coding"]
};
const clone = Object.assign({}, original);
// Modifying primitive properties on clone doesn't affect original
clone.name = "Bob";
console.log(original.name); // "Alice" (unchanged)
// BUT: nested objects are shared references!
clone.hobbies.push("gaming");
console.log(original.hobbies); // ["reading", "coding", "gaming"] — AFFECTED!Why Shallow Copy Matters
| Property Type | Clone Behavior | Original Affected? |
|---|---|---|
| String | Copied by value | No |
| Number | Copied by value | No |
| Boolean | Copied by value | No |
| Nested Object | Reference copied | Yes, if mutated |
| Array | Reference copied | Yes, if mutated |
| Date | Reference copied | Yes, if mutated |
| Function | Reference copied | No (functions are immutable) |
For truly independent copies of nested objects, you need a deep clone strategy:
// Deep clone with structuredClone (modern browsers/Node 17+)
const deepClone = structuredClone(original);
// Or with JSON (loses functions, Dates, undefined, etc.)
const jsonClone = JSON.parse(JSON.stringify(original));The Defaults Pattern
One of the most practical uses of Object.assign is applying default values to configuration objects:
function createServer(options) {
const defaults = {
host: "localhost",
port: 3000,
protocol: "http",
timeout: 5000,
maxConnections: 100,
logging: true
};
const config = Object.assign({}, defaults, options);
console.log(`Starting ${config.protocol}://${config.host}:${config.port}`);
console.log(`Timeout: ${config.timeout}ms, Max connections: ${config.maxConnections}`);
return config;
}
// Only override what you need
createServer({ port: 8080, protocol: "https" });
// Starting https://localhost:8080
// Timeout: 5000ms, Max connections: 100
// Use all defaults
createServer({});
// Starting http://localhost:3000Mutating the Target Object
When you pass an existing object as the target, Object.assign modifies it directly. This is useful for updating objects in place:
const state = {
user: null,
loading: false,
error: null
};
// Update state in place
Object.assign(state, {
user: { name: "Alice" },
loading: false,
error: null
});
console.log(state);
// { user: { name: "Alice" }, loading: false, error: null }Adding Methods to Prototypes
function User(name) {
this.name = name;
}
Object.assign(User.prototype, {
greet() {
return `Hi, I'm ${this.name}`;
},
toJSON() {
return { name: this.name };
},
toString() {
return this.name;
}
});
const user = new User("Alice");
console.log(user.greet()); // "Hi, I'm Alice"Object.assign vs Spread Operator
The spread operator ({ ...obj }) was introduced in ES2018 and provides a more readable syntax for most of the same operations:
// Both produce identical results for simple merges
const defaults = { theme: "light", fontSize: 14 };
const prefs = { theme: "dark" };
// Object.assign
const config1 = Object.assign({}, defaults, prefs);
// Spread operator
const config2 = { ...defaults, ...prefs };
console.log(config1); // { theme: "dark", fontSize: 14 }
console.log(config2); // { theme: "dark", fontSize: 14 }Comparison Table
| Feature | Object.assign | Spread {...} |
|---|---|---|
| Mutates target | Yes (when target is existing object) | No (always creates new object) |
| Syntax readability | Verbose | Clean and concise |
| Can copy to existing object | Yes | No |
| Triggers setters on target | Yes | No |
| Works in older environments | ES2015+ | ES2018+ |
| Dynamic source count | Easy (rest args) | Requires manual listing |
| Performance | Comparable | Comparable |
When to Use Object.assign Over Spread
// 1. When you WANT to mutate the target
const user = { name: "Alice" };
Object.assign(user, { age: 28 }); // Mutates `user` in place
// 2. When you need to assign to a specific target reference
const formState = existingReactiveObject;
Object.assign(formState, newValues); // Preserves reactivity in some frameworks
// 3. When building objects dynamically with variable source count
const sources = [obj1, obj2, obj3];
Object.assign({}, ...sources); // Spread sources easilyProperty Enumeration Rules
Object.assign only copies own, enumerable properties. It does not copy:
- Inherited properties (from the prototype chain)
- Non-enumerable properties
- Symbol properties are copied (unlike
for...in)
const proto = { inherited: true };
const source = Object.create(proto);
source.own = "value";
Object.defineProperty(source, "hidden", {
value: "secret",
enumerable: false
});
source[Symbol("id")] = 42;
const result = Object.assign({}, source);
console.log(result);
// { own: "value", Symbol(id): 42 }
// 'inherited' not copied (from prototype)
// 'hidden' not copied (non-enumerable)
// Symbol property IS copiedReal-World Example: API Request Builder
class ApiClient {
constructor(baseConfig = {}) {
this.config = Object.assign({
baseUrl: "https://api.example.com",
timeout: 10000,
headers: {
"Content-Type": "application/json"
}
}, baseConfig);
}
request(endpoint, options = {}) {
const requestConfig = Object.assign(
{},
this.config,
{
url: `${this.config.baseUrl}${endpoint}`,
headers: Object.assign({}, this.config.headers, options.headers)
},
options
);
console.log("Request:", requestConfig);
return requestConfig;
}
get(endpoint, options = {}) {
return this.request(endpoint, Object.assign({ method: "GET" }, options));
}
post(endpoint, data, options = {}) {
return this.request(endpoint, Object.assign({ method: "POST", body: JSON.stringify(data) }, options));
}
}
const client = new ApiClient({
headers: { "Authorization": "Bearer token123" }
});
client.get("/users", {
headers: { "X-Custom": "value" }
});
// Merges base config + request config + custom headersCommon Mistakes to Avoid
Forgetting the Empty Target
const defaults = { theme: "light" };
const prefs = { theme: "dark" };
// WRONG: mutates defaults!
const config = Object.assign(defaults, prefs);
console.log(defaults.theme); // "dark" — defaults is now corrupted!
// CORRECT: empty object as target
const config2 = Object.assign({}, defaults, prefs);
console.log(defaults.theme); // Still "light" in original situationExpecting Deep Merge
const defaults = {
database: { host: "localhost", port: 5432 }
};
const overrides = {
database: { port: 3306 }
};
// WRONG expectation: deep merge
const config = Object.assign({}, defaults, overrides);
console.log(config.database);
// { port: 3306 } — the ENTIRE database object was replaced!
// CORRECT: manually merge nested objects
const config2 = Object.assign({}, defaults, {
database: Object.assign({}, defaults.database, overrides.database)
});
console.log(config2.database);
// { host: "localhost", port: 3306 }Assigning null or undefined Sources
// null and undefined sources are silently skipped
const result = Object.assign({}, { a: 1 }, null, undefined, { b: 2 });
console.log(result); // { a: 1, b: 2 } — no errorBest Practices
- Always use
{}as the first argument unless you specifically want to mutate an existing object - Use spread for simple merges in modern code; reserve
Object.assignfor mutation patterns - Handle nested objects explicitly because
Object.assigndoes not deep merge - Validate inputs when source objects come from external data (type checking prevents runtime surprises)
- Prefer
structuredClone()for deep copies when available
Rune AI
Key Insights
- Syntax:
Object.assign(target, ...sources)copies own enumerable properties from sources to target - Mutates target: The first argument is modified in place and returned; use
{}to avoid side effects - Shallow only: Nested objects are copied by reference, not by value; deep merge requires manual handling
- Later wins: Properties from later sources overwrite earlier ones in left-to-right order
- Spread alternative:
{ ...a, ...b }provides cleaner syntax for non-mutating merges in ES2018+ code
Frequently Asked Questions
Does Object.assign do a deep copy?
Can Object.assign copy getters and setters?
What happens if a source property is a function?
Is Object.assign polyfillable for older browsers?
How does Object.assign handle arrays?
Conclusion
Object.assign is a fundamental JavaScript utility for copying and merging object properties. It handles default configurations, shallow cloning, prototype method assignment, and in-place state updates. Understanding its shallow copy behavior prevents bugs with nested objects, and knowing when to use spread instead keeps your code modern and readable. For most simple merges in new code, prefer the spread operator; reach for Object.assign when you need target mutation or dynamic source handling.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.