Implementing Custom Object.assign in JavaScript



Implementing a custom version of Object.assign is a great exercise to understand how object property assignment works in JavaScript. The Object.assign method is used to copy the values of all enumerable own properties from one or more source objects to a target object.


What is Object.assign?

Object.assign is a method used to copy the values of all enumerable own properties from one or more source objects to a target object. It returns the target object.

Real Interview Insights

Interviewers might ask you to:

  • Implement a custom Object.assign function.
  • Explain the difference between shallow and deep copying.
  • Handle edge cases such as non-object targets or sources.

Implementing Custom Object.assign

Here’s how you can implement a custom version of Object.assign:

function customAssign(target, ...sources) {
  if (target == null) {
    throw new TypeError('Cannot convert undefined or null to object');
  }
 
  const to = Object(target);
 
  sources.forEach(source => {
    if (source != null) {
      for (let key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
          to[key] = source[key];
        }
      }
 
      // Copying symbols (if the environment supports them)
      if (typeof Object.getOwnPropertySymbols === 'function') {
        const symbols = Object.getOwnPropertySymbols(source);
        symbols.forEach(symbol => {
          if (Object.prototype.propertyIsEnumerable.call(source, symbol)) {
            to[symbol] = source[symbol];
          }
        });
      }
    }
  });
 
  return to;
}
Explanation:
  • Parameter Check: Ensure the target is not null or undefined, throwing a TypeError if it is.
  • Convert to Object: Convert the target to an object if it’s not already one.
  • Iterate Over Sources: Loop through each source object and copy its own properties to the target object.
  • Copy Symbols: If symbols are supported, copy enumerable symbol properties as well.

Practical Example

Consider an example with multiple source objects:

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3, a: 4 };
const result = customAssign(target, source1, source2);
 
console.log(result); // Output: { a: 4, b: 2, c: 3 }
console.log(target); // Output: { a: 4, b: 2, c: 3 }

In this example:

  • Overwriting Properties: Properties in later source objects overwrite those in earlier ones.
  • Mutating the Target: The customAssign function mutates the target object and returns it.

Handling Edge Cases

  1. Primitive Targets: Convert non-object targets (like strings or numbers) to objects.
  2. Non-Enumerable Properties: Only copy enumerable properties.
  3. Symbol Properties: Handle copying of symbol properties if supported by the environment.

Enhanced Implementation with Edge Case Handling

function customAssign(target, ...sources) {
  if (target == null) {
    throw new TypeError('Cannot convert undefined or null to object');
  }
 
  const to = Object(target);
 
  sources.forEach(source => {
    if (source != null) {
      for (let key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
          to[key] = source[key];
        }
      }
 
      // Copying symbols (if the environment supports them)
      if (typeof Object.getOwnPropertySymbols === 'function') {
        const symbols = Object.getOwnPropertySymbols(source);
        symbols.forEach(symbol => {
          if (Object.prototype.propertyIsEnumerable.call(source, symbol)) {
            to[symbol] = source[symbol];
          }
        });
      }
    }
  });
 
  return to;
}
 
// Test cases
console.log(customAssign({ a: 1 }, { b: 2 }, { c: 3, a: 4 })); // { a: 4, b: 2, c: 3 }
console.log(customAssign({}, { a: 1 }, null, { b: 2 })); // { a: 1, b: 2 }
console.log(customAssign('string', { a: 1 })); // TypeError: Cannot convert undefined or null to object

Use Cases for Custom Object.assign

  1. Merging Objects: Combining multiple objects into one.
  2. Cloning Objects: Creating a shallow copy of an object.
  3. Default Parameters: Setting default values for function parameters.

Coding Challenge: Adding a Deep Copy Option

Challenge: Enhance the customAssign function to include an option for deep copying.

function customAssign(target, ...sources) {
  if (target == null) {
    throw new TypeError('Cannot convert undefined or null to object');
  }
 
  const to = Object(target);
 
  sources.forEach(source => {
    if (source != null) {
      for (let key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
          if (typeof source[key] === 'object' && source[key] !== null) {
            to[key] = customAssign({}, source[key]); // Deep copy
          } else {
            to[key] = source[key];
          }
        }
      }
 
      // Copying symbols (if the environment supports them)
      if (typeof Object.getOwnPropertySymbols === 'function') {
        const symbols = Object.getOwnPropertySymbols(source);
        symbols.forEach(symbol => {
          if (Object.prototype.propertyIsEnumerable.call(source, symbol)) {
            to[symbol] = source[symbol];
          }
        });
      }
    }
  });
 
  return to;
}
 
// Example usage with deep copy
const target = { a: { x: 1 } };
const source = { a: { y: 2 }, b: 3 };
const result = customAssign(target, source);
 
console.log(result); // Output: { a: { x: 1, y: 2 }, b: 3 }
console.log(target); // Output: { a: { x: 1, y: 2 }, b: 3 }

In this challenge:

  • Deep Copy Option: Recursively copy nested objects for a deep copy.