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)
| Parameter | Description |
|---|---|
target | The object to copy properties into (mutated) |
source1...N | One or more objects to copy properties from |
| Returns | The 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
| Feature | Object.assign(target, src) | { ...src } |
|---|---|---|
| Mutates target? | Yes (unless target is {}) | No (always new object) |
| Triggers setters on target? | Yes | No |
| Copies Symbol keys? | Yes | Yes |
| 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) |
| Syntax | Function call | Syntax (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
Object.assign(target, ...sources)copies own enumerable properties and mutates the target.- Use
Object.assign({}, ...)to avoid mutating originals. - It performs a shallow copy — nested objects share the same reference.
- Later sources win — properties are overwritten left to right.
- Spread
{...obj}is the modern alternative; prefer it for immutable patterns. Object.assigntriggers setters on the target; spread does not.
Explain-It Challenge
Explain without notes:
- What is the return value of
Object.assign(target, src)? Is it a new object? - You merge
Object.assign({}, a, b)— if bothaandbhave anameproperty, whose value wins? - Why does
Object.assignnot produce a true deep clone? What happens to nested objects?
Navigation: <- 1.24.a — Object.entries . 1.24.c — Object.freeze ->