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.

JavaScriptbeginner
13 min read

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

javascriptjavascript
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
javascriptjavascript
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 object

Merging Multiple Objects

Object.assign accepts multiple source objects. Properties from later sources overwrite earlier ones:

javascriptjavascript
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

javascriptjavascript
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:

javascriptjavascript
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 TypeClone BehaviorOriginal Affected?
StringCopied by valueNo
NumberCopied by valueNo
BooleanCopied by valueNo
Nested ObjectReference copiedYes, if mutated
ArrayReference copiedYes, if mutated
DateReference copiedYes, if mutated
FunctionReference copiedNo (functions are immutable)

For truly independent copies of nested objects, you need a deep clone strategy:

javascriptjavascript
// 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:

javascriptjavascript
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:3000

Mutating 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:

javascriptjavascript
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

javascriptjavascript
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:

javascriptjavascript
// 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

FeatureObject.assignSpread {...}
Mutates targetYes (when target is existing object)No (always creates new object)
Syntax readabilityVerboseClean and concise
Can copy to existing objectYesNo
Triggers setters on targetYesNo
Works in older environmentsES2015+ES2018+
Dynamic source countEasy (rest args)Requires manual listing
PerformanceComparableComparable

When to Use Object.assign Over Spread

javascriptjavascript
// 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 easily

Property 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)
javascriptjavascript
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 copied

Real-World Example: API Request Builder

javascriptjavascript
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 headers

Common Mistakes to Avoid

Forgetting the Empty Target

javascriptjavascript
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 situation

Expecting Deep Merge

javascriptjavascript
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

javascriptjavascript
// 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 error

Best Practices

  1. Always use {} as the first argument unless you specifically want to mutate an existing object
  2. Use spread for simple merges in modern code; reserve Object.assign for mutation patterns
  3. Handle nested objects explicitly because Object.assign does not deep merge
  4. Validate inputs when source objects come from external data (type checking prevents runtime surprises)
  5. Prefer structuredClone() for deep copies when available
Rune AI

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
RunePowered by Rune AI

Frequently Asked Questions

Does Object.assign do a deep copy?

No. Object.assign performs a shallow copy. [Primitive values](/tutorials/programming-languages/javascript/primitive-vs-reference-types-in-js-full-guide) are copied by value, but nested objects and arrays are copied by reference. If you modify a nested object in the copy, the original is also affected. Use `structuredClone()` for deep copies.

Can Object.assign copy getters and setters?

Object.assign invokes getters on the source and assigns the returned value as a plain data property on the target. It does not preserve the getter/setter [definitions](/tutorials/programming-languages/javascript/javascript-object-methods-a-complete-tutorial). If you need to copy property descriptors (including getters/setters), use `Object.getOwnPropertyDescriptors()` with `Object.defineProperties()`.

What happens if a source property is a function?

Functions are copied as regular property values. The function reference is shared between the source and target. Since functions are immutable, this effectively behaves like a copy. The function will work correctly on the target, with `this` binding determined by how it is called.

Is Object.assign polyfillable for older browsers?

Yes. Object.assign can be polyfilled for environments before ES2015. The MDN documentation includes a polyfill that covers all standard behavior except Symbol property copying. Modern build tools like Babel can automatically add this polyfill when targeting older browsers.

How does Object.assign handle arrays?

rrays are [objects](/tutorials/programming-languages/javascript/what-is-an-object-in-javascript-beginner-guide) with numeric keys. If you pass arrays to Object.assign, it merges by index: `Object.assign([1,2,3], [4,5])` produces `[4,5,3]` because indices 0 and 1 are overwritten. For [merging arrays](/tutorials/programming-languages/javascript/how-to-merge-two-arrays-in-javascript-full-guide), use spread or concat instead.

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.