Search code examples
javascriptobjectlodash

Why does the first object in _.merge get updated with the current value?


Say I have three objects.

const x = { val: 1 };
const y = { val: 2 };
const z = { val: 3 };

const mergedObj = _.merge(x,y,z);
// x = { val: 3 };
// y = { val: 2 };
// z = { val: 3 };
// mergedObj = { val: 3 };

Why is it that the first object in lodash's merge function changes object x to the value of 3 but object y remains at 2? I would think that all objects would retain the same key-value pairs since the _.merge function would create a new object? Am I wrong in that assumption?


Solution

  • Lodash mutates the first argument rather than creating a whole new object. Each of the subsequent arguments are applied on top of the first argument in order but they themselves are untouched.

    We can demonstrate this by creating a JavaScript setter function. This is called everytime the property is 'set' (thus it's name).

    let merge1 = { val: 1 };
    let merge2 = { val: 2 };
    let merge3 = { val: 3 };
    
    let mergeTarget = {
    	val:0,
      set val(name) {
        console.log("Setting with:" + name);
      }
    }
    
    _.merge(mergeTarget,merge1,merge2, merge3);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>

    You will see that the setter on the mergeTarget is not called once but three times with each of the subsequent properties in order:

    Setting with:1
    Setting with:2
    Setting with:3
    

    To be clear Lodash does return a reference to the final value, which can be useful in cases such as when using the merge inline. E.g (to use a contrived example):

    someArray.map(_.merge(mappingFunction, properties))
    

    However, as you have noticed, this return value is simply a reference to the first argument which has been mutated.

    Why?

    As for the deeper question of why lodash does it like this... This is a good question since, afterall, usually it makes sense to avoid impure functions for a simple utility function. Really one would have to ask the lodash developers and also the underscore developers before them who set this practice in place.

    But we can quite easily think of some good reasons why this was implemented like this and why it remains.

    1. Ease of use/style.

    Part of the appeal of Lodash/underscore is that they provide convenient and well defined utility functions that are easy to use liberally. To the extent that they can feel like syntactic sugar for JavaScript itself. In the case where you just need to merge an object it's one less thing to worry about or clutter the code to do it like this.

    E.g. you can do this:

    myComponent.updateConfig = function(update){
       _.merge(myComponent.config, update)
    }
    

    Which is just a bit neater than having to worry about the return value. And given that the side effects are clear and well defined, it's not a bad thing to use it like this.

    This is arguably the least important reason however.

    2. Performance.

    It's a sensible default to avoid creating excess objects unless necessary.

    JavaScript's default behaviour is to pass objects by reference and it doesn't mandate any clever way of doing copy-on-write mutation of just part of an object. If you need to create a modified copy of an object while leaving the original intact the only way of doing that is to copy the whole object and then make the modifications to the copy.

    This is not a problem when dealing with simple objects with only a few properties, but say you have a complex object with a few hundred properties, and you incrementally merge in additions as part of an app's update cycle (for example). It's not hard to imagine situations where optimisation and memory leaks could become a problem.

    3. Easily avoidable.

    There are situations where it is not desirable to mutate the object and we would prefer the pure functional behaviour. However it is trivially easy to create this behaviour by simply passing an empty object as the first parameter.

    const x = { val: 1 };
    const y = { val: 2 };
    const z = { val: 3 };
    
    const result = _.merge({},x,y,z);
    console.log(x.val) //will return 1
    result.val = 5
    console.log(x.val) //will still return 1