522

In the Lodash library, can someone provide a better explanation of merge and extend / assign.

Its a simple question but the answer evades me nonetheless.

yurisich
  • 6,991
  • 7
  • 42
  • 63
JDillon522
  • 19,046
  • 15
  • 47
  • 81

5 Answers5

628

Here's how extend/assign works: For each property in source, copy its value as-is to destination. if property values themselves are objects, there is no recursive traversal of their properties. Entire object would be taken from source and set in to destination.

Here's how merge works: For each property in source, check if that property is object itself. If it is then go down recursively and try to map child object properties from source to destination. So essentially we merge object hierarchy from source to destination. While for extend/assign, it's simple one level copy of properties from source to destination.

Here's simple JSBin that would make this crystal clear: http://jsbin.com/uXaqIMa/2/edit?js,console

Here's more elaborate version that includes array in the example as well: http://jsbin.com/uXaqIMa/1/edit?js,console

AdrieanKhisbe
  • 3,899
  • 8
  • 37
  • 45
Shital Shah
  • 63,284
  • 17
  • 238
  • 185
  • If I could upvote this answer twice I would. Thanks for the explanation and the clear examples. – JDillon522 Nov 14 '13 at 00:18
  • 20
    An important difference seems to be that while _.merge returns a new merged object, _.extend mutates the destination object in-place, – letronje Feb 13 '15 at 09:31
  • 88
    They both appear to mutate the destination object regardless of what they return. – Jason Rice Feb 13 '15 at 22:55
  • 7
    It also appears that _.extend clobbers members of the destination object if they are not present in the source object, which is suprising to me. – Jason Rice Feb 13 '15 at 22:58
  • Hey I'm baffled, I took the jsbin code from the first link and all I did was store the results in a variable before logging them instead of just logging them directly. But the results are different. Can anyone explain? http://jsbin.com/jedeguyuka/2/edit?js,console – Kevin Wheeler Jun 13 '15 at 00:59
  • 1
    @kevin 'dest' is modified after call to merge or extend. The return value from the call is actually 'dest' object itself. This is why you see this behavior. – Shital Shah Jun 17 '15 at 12:03
  • 6
    @JasonRice They don't get clobbered. [For example in this fiddle, the "a" property doesn't get clobbered](http://jsbin.com/tocavuqami/1/edit?js,console). It is true that after the extend, dest["p"]["y"] no longer exists -- This is because before the extend src and dest both had a "p" property, so dest's "p" property gets completely overwritten by src's "p" property (they are the exact same object now). – Kevin Wheeler Jun 17 '15 at 23:38
  • 24
    To be clear, both methods modify/overwrite the *first* argument by reference. So if you want a new object from the resulting merge, best to pass an object literal. `var combined = merge({}, src, dest)` – Jon Jaques Jan 19 '16 at 21:29
  • 1
    Use `lodash/fp`, the functional programming version of lodash, if you don't want mutations – Drenai Jul 27 '19 at 07:21
  • JSbin was throwing errors, so here's a codepen of both: https://codepen.io/geoidesic/pen/GRqZXZm?editors=1111 – geoidesic Oct 17 '20 at 08:52
606

Lodash version 3.10.1

Methods compared

  • _.merge(object, [sources], [customizer], [thisArg])
  • _.assign(object, [sources], [customizer], [thisArg])
  • _.extend(object, [sources], [customizer], [thisArg])
  • _.defaults(object, [sources])
  • _.defaultsDeep(object, [sources])

Similarities

  • None of them work on arrays as you might expect
  • _.extend is an alias for _.assign, so they are identical
  • All of them seem to modify the target object (first argument)
  • All of them handle null the same

Differences

  • _.defaults and _.defaultsDeep processes the arguments in reverse order compared to the others (though the first argument is still the target object)
  • _.merge and _.defaultsDeep will merge child objects and the others will overwrite at the root level
  • Only _.assign and _.extend will overwrite a value with undefined

Tests

They all handle members at the root in similar ways.

_.assign      ({}, { a: 'a' }, { a: 'bb' }) // => { a: "bb" }
_.merge       ({}, { a: 'a' }, { a: 'bb' }) // => { a: "bb" }
_.defaults    ({}, { a: 'a' }, { a: 'bb' }) // => { a: "a"  }
_.defaultsDeep({}, { a: 'a' }, { a: 'bb' }) // => { a: "a"  }

_.assign handles undefined but the others will skip it

_.assign      ({}, { a: 'a'  }, { a: undefined }) // => { a: undefined }
_.merge       ({}, { a: 'a'  }, { a: undefined }) // => { a: "a" }
_.defaults    ({}, { a: undefined }, { a: 'bb' }) // => { a: "bb" }
_.defaultsDeep({}, { a: undefined }, { a: 'bb' }) // => { a: "bb" }

They all handle null the same

_.assign      ({}, { a: 'a'  }, { a: null }) // => { a: null }
_.merge       ({}, { a: 'a'  }, { a: null }) // => { a: null }
_.defaults    ({}, { a: null }, { a: 'bb' }) // => { a: null }
_.defaultsDeep({}, { a: null }, { a: 'bb' }) // => { a: null }

But only _.merge and _.defaultsDeep will merge child objects

_.assign      ({}, {a:{a:'a'}}, {a:{b:'bb'}}) // => { "a": { "b": "bb" }}
_.merge       ({}, {a:{a:'a'}}, {a:{b:'bb'}}) // => { "a": { "a": "a", "b": "bb" }}
_.defaults    ({}, {a:{a:'a'}}, {a:{b:'bb'}}) // => { "a": { "a": "a" }}
_.defaultsDeep({}, {a:{a:'a'}}, {a:{b:'bb'}}) // => { "a": { "a": "a", "b": "bb" }}

And none of them will merge arrays it seems

_.assign      ({}, {a:['a']}, {a:['bb']}) // => { "a": [ "bb" ] }
_.merge       ({}, {a:['a']}, {a:['bb']}) // => { "a": [ "bb" ] }
_.defaults    ({}, {a:['a']}, {a:['bb']}) // => { "a": [ "a"  ] }
_.defaultsDeep({}, {a:['a']}, {a:['bb']}) // => { "a": [ "a"  ] }

All modify the target object

a={a:'a'}; _.assign      (a, {b:'bb'}); // a => { a: "a", b: "bb" }
a={a:'a'}; _.merge       (a, {b:'bb'}); // a => { a: "a", b: "bb" }
a={a:'a'}; _.defaults    (a, {b:'bb'}); // a => { a: "a", b: "bb" }
a={a:'a'}; _.defaultsDeep(a, {b:'bb'}); // a => { a: "a", b: "bb" }

None really work as expected on arrays

Note: As @Mistic pointed out, Lodash treats arrays as objects where the keys are the index into the array.

_.assign      ([], ['a'], ['bb']) // => [ "bb" ]
_.merge       ([], ['a'], ['bb']) // => [ "bb" ]
_.defaults    ([], ['a'], ['bb']) // => [ "a"  ]
_.defaultsDeep([], ['a'], ['bb']) // => [ "a"  ]

_.assign      ([], ['a','b'], ['bb']) // => [ "bb", "b" ]
_.merge       ([], ['a','b'], ['bb']) // => [ "bb", "b" ]
_.defaults    ([], ['a','b'], ['bb']) // => [ "a", "b"  ]
_.defaultsDeep([], ['a','b'], ['bb']) // => [ "a", "b"  ]
Selfish
  • 6,023
  • 4
  • 44
  • 63
Nate
  • 12,963
  • 4
  • 59
  • 80
  • 33
    It actually merge arrays exactly like it merge objects, because arrays are objects with numeric keys. I agree that one would expect to concatenate, or replace arrays, depends on usage. – Mistic Oct 21 '15 at 19:22
  • 13
    Excellent answer. The tests were very didactic :-) – Lucio Paiva Oct 25 '15 at 23:27
  • 5
    `_.extend is an alias for _.assign, so they are identical` conflicts with `Only _.assign will overwrite a value with undefined` – Chazt3n Apr 25 '16 at 19:51
  • Though, I'm not sure I agree that those statements directly conflict, I can see how it's unclear anyway. Edited. thanks! – Nate Apr 25 '16 at 21:00
  • 10
    As of v4.0, _.extend is now an alias for _.assignIn, not _assign. The assignIn function adds dealing with inherited properties. – Mike Hedman May 06 '16 at 06:55
  • 2
    is null treated the same as undifined here? – C_B Feb 06 '17 at 12:54
  • 1
    Good question. No, it's totally different in how it's handled from undefined. I'm adding that test case now. – Nate Feb 07 '17 at 14:35
  • 2
    lodash 4 has a `mergeWith(obj, sources, customizer)` method. The specific example given for customizer is one to implement array concatenation. – smendola Apr 14 '17 at 20:58
  • 1
    ... **However**, (as of 4.17.4) it seems to only work with arrays that are nested inside objects, and **not for top-level arrays**; e.g. this works as expected: `_.mergeWith({x:[1,2,3]}, {x:[4,5,6]}, mergeArr) ` which returns `{x:[1,2,3,4,5,6]}` but this: `_.mergeWith([1,2,3], [4,5,6], mergeArr)` disappoints with a return of `[4,5,6]` – smendola Apr 14 '17 at 21:14
  • That actually seems consistent with 3.x since merge is treating the root-level-array as an object with the array index as keys, so merging `[1,2,3]` and `[4,5,6]` would be similar to merging `{0:1,1:2, 2:3}` and `{0:4,1:5, 2:6}`. I find this confusing, but it's how it works. – Nate Apr 17 '17 at 20:07
84

Another difference to pay attention to is handling of undefined values:

mergeInto = { a: 1}
toMerge = {a : undefined, b:undefined}
lodash.extend({}, mergeInto, toMerge) // => {a: undefined, b:undefined}
lodash.merge({}, mergeInto, toMerge)  // => {a: 1, b:undefined}

So merge will not merge undefined values into defined values.

user456584
  • 86,427
  • 15
  • 75
  • 107
samz
  • 1,592
  • 3
  • 21
  • 37
  • 4
    Is it just me or does that make lodash.extend completely useless in that it always returns a clone of the 'toMerge' object? – Jason Rice Feb 13 '15 at 23:07
  • 6
    If `mergeInto` had properties that `toMerge` didn't have then it would retain those properties. In that case it wouldn't be a clone. – David Neale Feb 18 '15 at 12:11
  • 1
    @JasonRice remove the empty {} and it will merge it in place lodash.merge(mergeInto, toMerge) – sidonaldson May 15 '15 at 11:15
24

It might be also helpful to consider what they do from a semantic point of view:

_.assign

   will assign the values of the properties of its second parameter and so on,
   as properties with the same name of the first parameter. (shallow copy & override)

_.merge

   merge is like assign but does not assign objects but replicates them instead.
  (deep copy)

_.defaults

   provides default values for missing values.
   so will assign only values for keys that do not exist yet in the source.

_.defaultsDeep

   works like _defaults but like merge will not simply copy objects
   and will use recursion instead.

I believe that learning to think of those methods from the semantic point of view would let you better "guess" what would be the behavior for all the different scenarios of existing and non existing values.

epeleg
  • 10,347
  • 17
  • 101
  • 151
6

If you want a deep copy without override while retaining the same obj reference

obj = _.assign(obj, _.merge(obj, [source]))
Paul Rooney
  • 20,879
  • 9
  • 40
  • 61
mbao01
  • 160
  • 3
  • 6