Object.assign()
ES6+Copies all enumerable own properties (both string- and Symbol-keyed) from one or more source objects onto a target object and returns that same target. It performs a shallow merge: later sources overwrite earlier ones, and nested objects are copied by reference, not cloned.
Syntax
Object.assign(target, ...sources)Parameters
target Object The target object to copy to
sources Object The source object(s) to copy from
Return Value
The target object
Examples
const target = { a: 1 };
const source = { b: 2, c: 3 };
const result = Object.assign(target, source);
console.log(result); 📌 When to Use
Use Object.assign() to merge several objects into one, to apply defaults ahead of user-supplied options, or to add a batch of properties to an existing object — including cases where you deliberately want in-place mutation, which the spread operator cannot express. The classic pattern is Object.assign({}, defaults, overrides): properties are applied left to right, so anything the caller supplies wins over the defaults while untouched settings survive. Because it mutates and returns the target, it also updates shared objects in place — for example patching a state object that other code already holds a reference to, or copying computed values onto this inside a constructor. Two lesser-known niches keep it relevant next to spread syntax: it copies Symbol-keyed enumerable properties, and it triggers setters on the target, so assigning onto an object with accessor properties runs your setter logic instead of blindly defining data properties. For plain immutable-style copies in modern code, spread syntax reads better; reach for Object.assign() specifically when you need mutation semantics, a dynamic list of sources via Object.assign({}, ...sourcesArray), or setter invocation. Remember it is shallow — merging configs with nested sections needs per-section handling or a deep-merge utility.
⚠️ Common Mistakes
Merging configs with nested sections and losing defaults - Object.assign({}, defaults, user) replaces an entire nested object when the user supplies any part of it: a user value of { headers: { auth: "x" } } wipes out every default header. Merge nested sections explicitly or use a deep-merge helper for layered configuration.
Assuming getters are copied as getters - Object.assign() reads each source getter once and copies the resulting snapshot as a plain data property. Computed properties stop updating on the copy; use Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) when accessors must be preserved.
Relying on partial results after an exception - if a setter on the target throws mid-merge, properties assigned before the error stay applied, leaving the target half-updated. Treat merges into live objects as non-atomic and validate sources before assigning.
Expecting deep cloning - Object.assign() only does shallow copy. Nested objects are copied by reference, not cloned.
Forgetting that the first argument (target) is modified - if you want a new object, always use an empty object as the first argument.
Not handling null or undefined sources - Object.assign() skips null/undefined sources, but this silent behavior can mask bugs.
✅ Best Practices
Merge a dynamic number of sources by spreading an array of objects: Object.assign({}, ...layers) folds an ordered list of config layers (defaults, environment, user) into one object without writing a reduce loop, and precedence stays obvious from array order.
Never use Object.assign() (or spread) as a deep-clone tool - for real copies of nested data use structuredClone(), which correctly handles Dates, Maps, Sets, typed arrays, and circular references that the JSON round-trip trick corrupts or throws on.
Prefer spread syntax {...obj} for simple cloning in modern code - it is more readable and commonly understood.
Use Object.assign({}, defaults, userOptions) pattern for merging defaults with user-provided options.
For deep cloning, use structuredClone() (modern browsers) or JSON.parse(JSON.stringify(obj)) for JSON-safe objects.
⚡ Performance Notes
Object.assign() is O(n) over the total number of properties in all sources. In modern engines it performs comparably to spread syntax — both use similar fast paths, and benchmark winners flip between engine versions, so choose by semantics rather than micro-speed. What matters more is object shape: merging sources whose keys arrive in a consistent order produces targets with shared hidden classes, keeping later property access monomorphic and fast; building objects with wildly varying key sets pushes call sites megamorphic and slows every downstream read. Assigning onto a target that has accessor properties is slower than onto plain data properties because each write runs a setter call. For hot paths that repeatedly rebuild small option objects, reusing one template shape (same keys, same order) is a bigger win than switching between assign and spread. Deep copies need structuredClone() regardless — assign cannot provide them.
🌍 Real World Example
API Request Configuration Builder
This request builder layers three sources — library defaults, per-call options, and the URL — into a single config with Object.assign(), demonstrating the left-to-right precedence that lets options override defaults. It then rebuilds the nested headers object separately, which is the honest fix for shallow-merge semantics: without that second assign, any caller passing one custom header would silently drop the default Content-Type and Accept headers. The same layering pattern underpins most HTTP clients and plugin systems found in real codebases.
const defaultConfig = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: 5000,
retries: 3
};
function createRequest(url, options = {}) {
// Merge defaults with user options
const config = Object.assign({}, defaultConfig, options, { url });
// Handle nested headers separately (shallow copy issue)
config.headers = Object.assign({}, defaultConfig.headers, options.headers);
return config;
}
const getUsers = createRequest('/api/users');
console.log(getUsers);
// { method: 'GET', headers: {...}, timeout: 5000, retries: 3, url: '/api/users' }
const createUser = createRequest('/api/users', {
method: 'POST',
headers: { 'Authorization': 'Bearer token123' },
timeout: 10000
});
console.log(createUser.method); // 'POST'
console.log(createUser.headers);
// { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer token123' }