How to Merge Objects in JavaScript

How to Merge Objects in JavaScript
Table of Contents

When manipulating data in JavaScript, one of the most common challenges developers face is merging objects. Whether you’re looking to combine properties or need to weave together complex nested structures intricately, JavaScript offers a variety of methods to achieve your objectives. In this blog post, we’ll dive into five effective ways to merge two objects in JavaScript, covering both shallow and deep merging techniques. Let’s enhance your coding toolkit with these practical solutions.

Understanding Object Merging

Object merging involves combining two or more objects into a single object and aggregating their properties. Depending on the merging depth, we differentiate between shallow and deep merges:

  • Shallow Merging: Combines top-level properties. Nested objects or arrays within the source objects are not recursively merged but replaced or copied directly.
  • Deep Merging: Involves recursively merging properties of nested objects, ensuring that each level of the object structure is combined accurately.

Shallow Merging with Spread Operator and Object.assign()

Shallow merging is straightforward in JavaScript, thanks to modern ES6 features like the spread operator (...) and the Object.assign() method.

Using the Spread Operator

Introduced in ES6, the spread operator (...) has become a go-to for many developers due to its concise syntax and ease of use. Perfect for shallow merging, it allows you to effortlessly combine the properties of two or more objects into a new one. If any properties overlap, the last object’s values take precedence, seamlessly overwriting previous entries.

const obj1 = { name: 'Alice', age: 25 }
const obj2 = { name: 'Bob', city: 'New York' }
const mergedObj = { ...obj1, ...obj2 }

console.log(mergedObj) // Output: { name: "Bob", age: 25, city: "New York" }

Utilizing Object.assign()

Object.assign() is another popular choice for shallow merging. This method copies the properties from one or more source objects to a target object, which it returns. Like the spread operator, it overwrites properties in the target object with those from the source objects if they share the same name. However, a key difference is that Object.assign() modifies the target object in place, which can be both a benefit and a drawback, depending on your needs.

const obj1 = { name: 'Alice', age: 25 }
const obj2 = { name: 'Bob', city: 'New York' }
const obj3 = Object.assign({}, obj1, obj2)

console.log(obj3); // Output: { name: "Bob", age: 25, city: "New York" }

Deep Merging with Lodash

When your project requires deep merging, the Lodash library offers a powerful solution with its merge() function. This method goes beyond the surface, recursively merging objects at all levels, making it ideal for complex data structures. Lodash’s merge() ensures that every layer of nested properties is combined, offering a level of depth that native JavaScript methods don’t directly provide.

const _ = require('lodash')
const obj1 = { a: 1, nested: { x: 2 } }
const obj2 = { b: 3, nested: { y: 4 } }
const mergedObj = _.merge(obj1, obj2)

console.log(mergedObj) // Output: { a: 1, nested: { x: 2, y: 4 }, b: 3 }

Writing Custom Merge Functions

Sometimes, the task at hand calls for a tailor-made solution. Writing your shallow merge function can be a great way to get precisely what you need without any extra fluff. This approach gives you complete control over the merging process, allowing you to adjust the behavior to fit your specific requirements.

function shallowMerge(obj1, obj2) {
  return Object.assign({}, obj1, obj2)
}

Deep Merge with a Custom Function

For those needing the ultimate control over their merging logic, crafting your deep merge function is the way to go. This method requires more effort; you’ll need to navigate each object’s properties and merge them accordingly recursively. However, the payoff is a highly customizable function that perfectly suits your project’s needs.

/**
 * Checks if the given item is a plain object, excluding arrays and dates.
 *
 * @param {*} item - The item to check.
 * @returns {boolean} True if the item is a plain object, false otherwise.
 */
function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Date))
}

/**
 * Recursively merges properties from source objects into a target object. If a property at the current level is an object,
 * and both target and source have it, the property is merged. Otherwise, the source property overwrites the target property.
 * This function does not modify the source objects and prevents prototype pollution by not allowing __proto__, constructor,
 * and prototype property names.
 *
 * @param {Object} target - The target object to merge properties into.
 * @param {...Object} sources - One or more source objects from which to merge properties.
 * @returns {Object} The target object after merging properties from sources.
 */
function deepMerge(target, ...sources) {
  if (!sources.length) return target
  
  // Iterate through each source object without modifying the sources array
  sources.forEach(source => {
    if (isObject(target) && isObject(source)) {
      for (const key in source) {
        if (isObject(source[key])) {
          if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
            continue // Skip potentially dangerous keys to prevent prototype pollution.
          }

          if (!target[key] || !isObject(target[key])) {
            target[key] = {}
          }

          deepMerge(target[key], source[key])
        } else {
          target[key] = source[key]
        }
      }
    }
  })

  return target
}

Performance Considerations

When merging objects, especially in performance-critical applications, understanding the impact of your chosen method is key.

  • Spread Operator and Object.assign(): These are efficient for shallow merges but can be less optimal for merging large objects due to the creation of new objects and the potential for memory overhead.
  • Lodash’s merge(): While powerful for deep merging, it may introduce performance overhead for complex nested structures due to its recursive nature.
  • Custom Functions: Tailoring a merge function to your specific use case can optimize performance but requires careful testing and optimization.

Optimizing Merge Operations

  • Profile and Test: Use profiling tools to measure the performance impact of merging operations in your application.
  • Avoid Deep Merging When Possible: Consider structuring your data to minimize the need for deep merges.
  • Reuse Objects: Reusing objects, when possible, can reduce memory allocations and garbage collection overhead.
  • Batch Operations: Look for opportunities to batch merge operations or leverage more efficient data structures.

Wrapping Up

Merging objects in JavaScript can be a manageable task. Whether you opt for the straightforwardness of the spread operator, the utility of Object.assign(), the depth of Lodash’s merge(), or decide to craft your custom functions, each method has unique advantages. Understanding these techniques and when to apply them will undoubtedly make you a more versatile and effective developer. Happy coding!

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.