Episode 1 — Fundamentals / 1.24 — Object Methods

1.24.b — Object.assign()

In one sentence: Object.assign(target, ...sources) copies all own enumerable string-keyed properties from one or more source objects to a target object, mutating and returning the target.

Navigation: <- 1.24.a — Object.entries . 1.24.c — Object.freeze ->


1. What does Object.assign() do?

It copies properties from source objects into a target object. The target is mutated in place and also returned.

const target = { a: 1 };
const source = { b: 2, c: 3 };

const result = Object.assign(target, source);

console.log(target);  // { a: 1, b: 2, c: 3 }
console.log(result);  // { a: 1, b: 2, c: 3 }
console.log(result === target);  // true  -- same reference!

2. Syntax

Object.assign(target, source1, source2, ...sourceN)
ParameterDescription
targetThe object to copy properties into (mutated)
source1...NOne or more objects to copy properties from
ReturnsThe target object

3. MUTATES the target object

This is the most important thing to remember. Object.assign does not create a new object — it modifies target directly.

const defaults = { theme: "light", lang: "en" };
const userPrefs = { theme: "dark" };

// DANGER: this mutates defaults!
Object.assign(defaults, userPrefs);
console.log(defaults);  // { theme: "dark", lang: "en" }  -- defaults changed!

Safe pattern — use an empty object as the target:

const defaults = { theme: "light", lang: "en" };
const userPrefs = { theme: "dark" };

const merged = Object.assign({}, defaults, userPrefs);
console.log(merged);    // { theme: "dark", lang: "en" }
console.log(defaults);  // { theme: "light", lang: "en" }  -- untouched

4. Shallow copy only

Object.assign performs a shallow copy. Nested objects are shared by reference, not cloned.

const original = {
  name: "Alice",
  address: { city: "NYC", zip: "10001" },
};

const clone = Object.assign({}, original);

clone.name = "Bob";                 // independent
clone.address.city = "LA";          // shared reference!

console.log(original.name);         // "Alice"  -- unaffected
console.log(original.address.city); // "LA"     -- MUTATED!

Why? The address property value is an object reference. Object.assign copies the reference, not the nested object.

Deep copy alternatives

// 1. structuredClone (modern, recommended)
const deep1 = structuredClone(original);

// 2. JSON round-trip (loses functions, Dates, undefined, etc.)
const deep2 = JSON.parse(JSON.stringify(original));

5. Merging multiple objects

Sources are applied left to right. Later sources overwrite earlier ones (last wins).

const base     = { a: 1, b: 2, c: 3 };
const override = { b: 20, d: 40 };
const extra    = { c: 300, e: 500 };

const merged = Object.assign({}, base, override, extra);
console.log(merged);
// { a: 1, b: 20, c: 300, d: 40, e: 500 }

This is the classic defaults + user config pattern:

function createConnection(userConfig) {
  const defaults = {
    host: "localhost",
    port: 3000,
    timeout: 5000,
    retries: 3,
  };
  const config = Object.assign({}, defaults, userConfig);
  // userConfig properties override defaults
  return config;
}

console.log(createConnection({ port: 8080, retries: 1 }));
// { host: "localhost", port: 8080, timeout: 5000, retries: 1 }

6. Only copies own enumerable properties

Like Object.entries(), Object.assign respects the same rules:

const proto = { inherited: true };
const obj = Object.create(proto);
obj.own = "yes";

Object.defineProperty(obj, "hidden", {
  value: "secret",
  enumerable: false,
});

obj[Symbol("id")] = 42;  // Symbol keys ARE copied by assign!

const copy = Object.assign({}, obj);
console.log(copy);
// { own: "yes", [Symbol(id)]: 42 }
// inherited: excluded (not own)
// hidden: excluded (not enumerable)

Note: Unlike Object.entries, Object.assign does copy Symbol-keyed properties.


7. Getters and setters are NOT preserved

Object.assign invokes the getter on the source and assigns the resulting value as a plain data property on the target:

const source = {
  get greeting() {
    return "Hello!";
  },
};

const copy = Object.assign({}, source);

console.log(copy.greeting);  // "Hello!"
console.log(Object.getOwnPropertyDescriptor(copy, "greeting"));
// { value: "Hello!", writable: true, enumerable: true, configurable: true }
// It is a plain value, NOT a getter

If the target has a setter, Object.assign will invoke that setter:

const target = {
  _name: "",
  set name(val) {
    this._name = val.toUpperCase();
  },
};

Object.assign(target, { name: "alice" });
console.log(target._name);  // "ALICE"  -- setter was invoked

8. Spread operator {...obj} as modern alternative

The spread syntax achieves the same result as Object.assign({}, obj) for most use cases:

// These are equivalent for simple merges:
const merged1 = Object.assign({}, defaults, userPrefs);
const merged2 = { ...defaults, ...userPrefs };

Comparison table

FeatureObject.assign(target, src){ ...src }
Mutates target?Yes (unless target is {})No (always new object)
Triggers setters on target?YesNo
Copies Symbol keys?YesYes
Copies getters as getters?No (invokes, copies value)No (invokes, copies value)
Works on prototype target?Yes (can assign to existing obj)No (creates plain object)
SyntaxFunction callSyntax (cannot polyfill)

When to prefer Object.assign

// 1. When you NEED to mutate an existing object
Object.assign(existingState, newData);

// 2. When the target has setters you want triggered
Object.assign(reactiveProxy, { name: "Alice" });

// 3. When building onto a specific prototype
const obj = Object.create(SomeProto);
Object.assign(obj, { method() {} });

When to prefer spread

// 1. Immutable patterns (always creates new object)
const next = { ...current, updated: true };

// 2. Readability in function args
doSomething({ ...defaults, ...overrides });

9. Error handling

If a property assignment throws (e.g., target property is non-writable), properties already copied remain, but later ones are skipped:

const target = {};
Object.defineProperty(target, "x", { value: 1, writable: false });

try {
  Object.assign(target, { x: 2, y: 3 });
} catch (e) {
  console.log(e);         // TypeError in strict mode
  console.log(target.y);  // undefined -- y was not copied
}

10. Real-world examples

Clone with modifications

const user = { name: "Alice", age: 30, role: "user" };
const admin = Object.assign({}, user, { role: "admin", permissions: ["all"] });

console.log(admin);
// { name: "Alice", age: 30, role: "admin", permissions: ["all"] }

Merging component options (framework pattern)

function createWidget(options) {
  const defaults = {
    width: 300,
    height: 200,
    color: "#333",
    animate: true,
    duration: 300,
  };
  return Object.assign({}, defaults, options);
}

const widget = createWidget({ width: 500, animate: false });
// { width: 500, height: 200, color: "#333", animate: false, duration: 300 }

Adding methods to a prototype

function Person(name) {
  this.name = name;
}

Object.assign(Person.prototype, {
  greet() {
    return `Hi, I'm ${this.name}`;
  },
  toString() {
    return `Person(${this.name})`;
  },
});

const p = new Person("Alice");
console.log(p.greet());     // "Hi, I'm Alice"
console.log(p.toString());  // "Person(Alice)"

Combining partial updates (state management)

let state = { count: 0, loading: false, error: null };

function updateState(partial) {
  state = Object.assign({}, state, partial);
}

updateState({ loading: true });
// { count: 0, loading: true, error: null }

updateState({ count: 5, loading: false });
// { count: 5, loading: false, error: null }

Key takeaways

  1. Object.assign(target, ...sources) copies own enumerable properties and mutates the target.
  2. Use Object.assign({}, ...) to avoid mutating originals.
  3. It performs a shallow copy — nested objects share the same reference.
  4. Later sources win — properties are overwritten left to right.
  5. Spread {...obj} is the modern alternative; prefer it for immutable patterns.
  6. Object.assign triggers setters on the target; spread does not.

Explain-It Challenge

Explain without notes:

  1. What is the return value of Object.assign(target, src)? Is it a new object?
  2. You merge Object.assign({}, a, b) — if both a and b have a name property, whose value wins?
  3. Why does Object.assign not produce a true deep clone? What happens to nested objects?

Navigation: <- 1.24.a — Object.entries . 1.24.c — Object.freeze ->