ESC

Type to search the knowledge base.

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.

beginner5 min read
  • 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 / undefined targets throw. The boxed object is what you mutate and return.
  • Skip null and undefined sources. 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.keys is 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

  1. Throw if target is null or undefined. to = Object(target).
  2. For each source, skip nullish, else from = Object(source).
  3. For each own enumerable string key, to[key] = from[key].
  4. Optionally copy enumerable own symbols the same way.
  5. Return to (the mutated target), not a fresh object.

Interview out-loud answer

Object.assign mutates the target and returns it. I coerce the target with Object(...) and throw on null/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 add getOwnPropertySymbols if we have time. This is a shallow copy. A throw in the middle leaves a half-updated target.

Further reading

Related guides