Shallow merge vs. Deep merge in JavaScript

A shallow merge in JavaScript is a method of merging two objects that combines the properties of both objects at the top level, but does not merge the properties of any nested objects. This means that if both objects have a property with the same key, the value of that key in the final object will be the value of the second object, and any nested objects will be overwritten by the second object's nested object.

Here is an example of shallow merge:

let obj1 = { a: 1, b: { x: 2, y: 3 } };
let obj2 = { b: { x: 4 }, c: 5 };
let shallowMerge = Object.assign({}, obj1, obj2);
console.log(shallowMerge); // { a: 1, b: { x: 4 }, c: 5 }

Here, the properties of obj1 and obj2 are combined at the top level, but the nested object b in obj1 is overwritten by the nested object b in obj2.

On the other hand, a deep merge is a method of merging two objects that also combines the properties of any nested objects. This means that if both objects have a property with the same key, the value of that key in the final object will be the merged version of the two objects' values, and any nested objects will also be merged recursively.

Here is an example of deep merge using lodash library

let obj1 = { a: 1, b: { x: 2, y: 3 } };
let obj2 = { b: { x: 4 }, c: 5 };
let deepMerge = _.merge({}, obj1, obj2);
console.log(deepMerge); // { a: 1, b: { x: 4, y: 3 }, c: 5 }

Here, the properties of obj1 and obj2 are combined at the top level, but the nested object b in obj1 is merged with the nested object b in obj2, so it will keep the properties of both objects.

Here's an example of how to perform a deep merge in pure JavaScript:

let obj1 = { a: 1, b: { x: 2, y: 3 }, c: [1, 2, 3] };
let obj2 = { b: { x: 4 }, c: [4, 5], d: "new value" };

function deepMerge(target, ...sources) {
    sources.forEach(source => {
        Object.keys(source).forEach(key => {
            if(typeof target[key] === 'object' && typeof source[key] === 'object') {
                deepMerge(target[key], source[key]);
            } else {
                target[key] = source[key];
            }
        });
    });
    return target;
}

let mergedObj = deepMerge({}, obj1, obj2);
console.log(mergedObj); // Outputs -> { a: 1, b: { x: 4, y: 3 }, c: [ 4, 5, 3 ], d: 'new value' }

In this example, we defined a function deepMerge() that takes in two or more objects and returns a new object that contains all the properties from the input objects. This function uses two nested loops, the outer loop iterates through the sources objects, and the inner loop iterates through the keys of the current source object. For each key, the function checks if the value of the key in the target object is an object and also the value of the key in the source object is an object, if so the function calls itself recursively to merge the nested objects. If the value of the key in the target object is not an object or the value of the key in the source object is not an object, then it assigns the value of the key in the source object to the key in the target object.

In this example, the deep merge correctly combines the properties of obj1 and obj2, including the nested object b and the array c and also adds the new key d from obj2 to the final object.

The function deepMerge() method will mutate the target object. if you want to keep the original objects intact, you can create a copy of the target object before calling the function.