Implement a polyfill for Object.assign
Object.assign(target, ...sources) copies enumerable own properties from sources onto target, left to right, and returns target. Polyfill: validate target isn't null/undefined, Object() it, loop sources, copy own enumerable keys (use hasOwnProperty), return target. Note it's a shallow copy.
Object.assign(target, ...sources) copies enumerable own properties from each source onto the target (left → right, later sources win) and returns the target. The polyfill tests whether you know those exact semantics.
The polyfill
function objectAssign(target, ...sources) {
// 1. target can't be null/undefined
if (target === null || target === undefined) {
throw new TypeError("Cannot convert undefined or null to object");
}
// 2. coerce target to an object (primitives get boxed)
const to = Object(target);
for (const source of sources) {
// 3. skip null/undefined sources (allowed, just ignored)
if (source === null || source === undefined) continue;
const from = Object(source);
// 4. copy OWN ENUMERABLE properties only
for (const key of Object.keys(from)) {
to[key] = from[key];
}
// 4b. also copy own enumerable Symbol keys (full spec correctness)
for (const sym of Object.getOwnPropertySymbols(from)) {
if (Object.prototype.propertyIsEnumerable.call(from, sym)) {
to[sym] = from[sym];
}
}
}
return to; // 5. return the (mutated) target
}The semantics you must get right
- Throws on null/undefined target —
Object.assign(null, ...)is aTypeError. - **null/undefined sources are skipped** silently — not an error.
- Only own enumerable properties — inherited (prototype) properties and non-enumerable ones are not copied.
Object.keysalready filters to own + enumerable, which is why it's the right loop. - Later sources overwrite earlier ones — left-to-right.
- Returns the target — and it mutates the target in place (that's why
Object.assign({}, a, b)is the copy idiom — fresh{}as target). - Symbol keys — full correctness copies own enumerable Symbol-keyed properties too.
The caveat: shallow copy
Object.assign copies values. For nested objects it copies the reference, not a deep clone — Object.assign({}, original).nested === original.nested. For deep copies, use structuredClone.
The framing
"Object.assign copies enumerable own properties from sources onto a target, left to right, and returns the mutated target. The polyfill: throw if the target is null/undefined, Object()-coerce it, loop the sources skipping null/undefined ones, and copy own enumerable keys — Object.keys conveniently gives exactly own+enumerable, and for full spec correctness also copy own enumerable symbols. The semantics that matter are: throws on a null target but skips null sources, own-enumerable-only, last write wins, returns target — and it's a shallow copy, so nested objects are shared by reference."
Follow-up questions
- •Why use Object.keys rather than a for...in loop?
- •What's the difference between a null target and a null source?
- •Why is Object.assign({}, a, b) the copy idiom?
- •Is Object.assign a deep or shallow copy?
Common mistakes
- •Using for...in, which also copies inherited enumerable properties.
- •Not throwing on a null/undefined target.
- •Throwing (instead of skipping) on null/undefined sources.
- •Forgetting to return the target.
- •Claiming it's a deep copy.
Performance considerations
- •O(total keys across sources). It's a shallow copy, so it's cheap relative to a deep clone — but mutating a large target repeatedly in a loop is O(n·m); prefer building one assign call.
Edge cases
- •Primitive target (gets boxed via Object()).
- •Symbol-keyed properties.
- •Getters on sources — they're invoked, the returned value is copied.
- •Non-enumerable properties — not copied.
- •Overlapping keys across sources — last wins.
Real-world examples
- •Merging default options with user-provided options.
- •Shallow-cloning objects for immutable updates (before object spread was common).