Object.assign Polyfill
Copy enumerable own properties onto a target. Getters that run, symbols, null/undefined sources skipped, and the mutation-in-place return value.
- javascript
- objects
- polyfill
- interview
Object.assign(target, ...sources) is not a clone and not a merge that returns a new object unless you pass {} as the target. It mutates target and returns that same reference. Later sources win. Getters on the source run. Setters on the target run. Inherited keys stay put. null and undefined sources are skipped. null and undefined targets throw.
A polyfill that only loops for...in copies the prototype chain and fails the prompt. One that uses Object.keys and stops there is the string-key subset. Enumerable symbols are extra credit the spec includes.
Key takeaways
ToObject(target)first.null/undefinedtargets throw. The boxed object is what you mutate and return.- Skip
nullandundefinedsources. Other primitives wrap; strings contribute'0','1', …- Copy own enumerable keys.
[[Get]]then[[Set]]. Later sources overwrite.- Symbols: spec copies enumerable own symbols. Mention them if
Object.keysis all you wrote.
Own enumerable strings, later sources win
function objectAssign(target, ...sources) {
if (target === null || target === undefined) {
throw new TypeError('Cannot convert undefined or null to object');
}
const to = Object(target);
for (const source of sources) {
if (source === null || source === undefined) continue;
const from = Object(source);
for (const key of Object.keys(from)) {
to[key] = from[key]; // Get source, Set target
}
}
return to;
}
const target = { a: 1, b: 1 };
const out = objectAssign(target, { b: 2, c: 3 }, { c: 4 });
out === target; // true
target; // { a: 1, b: 2, c: 4 }
Standalone function first. Patching Object.assign is interview convention and mutates a shared builtin.
Target vs sources
| Input | Behavior |
|---|---|
Object.assign(null, { a: 1 }) |
TypeError |
Object.assign(undefined, { a: 1 }) |
TypeError |
Object.assign({}, null, undefined, { a: 1 }) |
{ a: 1 } (nullish sources skipped) |
Object.assign(3, { a: 1 }) |
Number object with a: 1 (typeof is 'object') |
Object.assign({}, 'ab') |
{ '0': 'a', '1': 'b' } |
Object.assign({}, true, 10) |
{} (boolean and number wrappers have no enumerable own keys) |
Object(target) is ToObject. For an already-object target it is identity, which is why mutation is in place.
Getters run, descriptors do not copy
Assignment is to[key] = from[key]:
const source = {
get b() {
return 2;
},
};
const copy = objectAssign({}, source);
Object.getOwnPropertyDescriptor(copy, 'b');
// { value: 2, writable: true, enumerable: true, configurable: true }
// getter is gone; you copied the result
If the target has a setter for b, that setter runs instead of a data write. If the target has a non-writable b, assignment throws in strict mode and copying stops. Earlier keys from that source, and whole earlier sources, have already mutated target. The method is not transactional.
Inherited enumerable properties are not copied (Object.keys is own-only). Non-enumerable own properties are not copied. That is the usual “clone the class instance” failure.
const proto = { inherited: 1 };
const src = Object.create(proto, {
hidden: { value: 2, enumerable: false },
visible: { value: 3, enumerable: true },
});
objectAssign({}, src); // { visible: 3 }
Symbols as extra credit
The spec walks [[OwnPropertyKeys]], which includes symbols, and copies those that are enumerable.
function objectAssignWithSymbols(target, ...sources) {
if (target === null || target === undefined) {
throw new TypeError('Cannot convert undefined or null to object');
}
const to = Object(target);
for (const source of sources) {
if (source === null || source === undefined) continue;
const from = Object(source);
for (const key of Object.keys(from)) {
to[key] = from[key];
}
for (const sym of Object.getOwnPropertySymbols(from)) {
const desc = Object.getOwnPropertyDescriptor(from, sym);
if (desc && desc.enumerable) {
to[sym] = from[sym];
}
}
}
return to;
}
Object.keys never sees symbols. If you skip this loop, say so. A one-line for...in plus hasOwn still misses symbols (for...in is strings only).
Shallow copy, not deep
Nested objects are shared refs. objectAssign({}, { nested: { n: 1 } }).nested is the same object. For a graph copy use structuredClone or a real deep clone. Spread { ...obj } is the same shallow assign for string keys (spread also copies enumerable symbols in modern engines).
for...in plus hasOwn is a common wrong polyfill. for...in still visits enumerable inherited keys unless you filter, and it never sees symbols. Object.keys is the right string-key loop. Key order follows [[OwnPropertyKeys]]: integer indexes ascending, then other strings in insertion order, then symbols. Later sources still overwrite by name, so order across sources is “walk sources left to right,” not “sorted keys.”
Do not use Object.defineProperties here unless the prompt asked to copy descriptors. Assign is Get + Set. That is why a getter becomes a data property on the target, and why a frozen target throws instead of skipping.
Procedure
- Throw if
targetisnullorundefined.to = Object(target). - For each source, skip nullish, else
from = Object(source). - For each own enumerable string key,
to[key] = from[key]. - Optionally copy enumerable own symbols the same way.
- Return
to(the mutated target), not a fresh object.
Interview out-loud answer
Object.assignmutates the target and returns it. I coerce the target withObject(...)and throw onnull/undefined. Nullish sources are skipped. I copy own enumerable string keys with Get-then-Set, so getters fire and later sources win. Inherited and non-enumerable keys stay behind. Symbols are in the spec; I would addgetOwnPropertySymbolsif we have time. This is a shallow copy. A throw in the middle leaves a half-updated target.
Related on this site
- Deep vs Shallow Copy: assign is the shallow one
- Object.create Polyfill: allocate a target with a chosen prototype, then assign
- Getters and Setters: why
to[key] = from[key]is notdefineProperty - Symbols in JavaScript: enumerable symbol keys
- Implement JSON.stringify: another own-enumerable-string walk
- Rest and Spread Syntax: object spread as syntax for the same idea
Further reading
Related guides
- Object.entries and fromEntriesRound-trip own enumerable string keys with Object.entries and Object.fromEntries. Not a Map, not symbols, and later duplicate keys win.
- Object.keys PolyfillPolyfill Object.keys: own enumerable string keys in insertion order, no symbols, and integer-index keys listed before other strings.
- Array.filter PolyfillWrite filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- Array.find PolyfillWrite Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.
- Array.flat PolyfillImplement flat(depth) with Infinity, holes, and non-array array-likes. Recursion vs stack, and why concat(spread) blows up on huge inputs.