JavaScript Deep Merge

By  on  

I recently shared how you can merge object properties with the spread operator but this method has one big limitation:  the spread operator merge isn't a "deep" merge, meaning merges are recursive.  Moreover nested object properties aren't merged -- the last value specified in the merge replaces the last, even when there are other properties that should exist.

const defaultPerson = {
  name: 'Anon',
  gender: 'Female',
  hair: {
    color: 'brown',
    cut: 'long'
  },
  eyes: 'blue',
  family: ['mom', 'dad']
};

const me = {
  name: 'David Walsh',
  gender: 'Male',
  hair: {
    cut: 'short'
  },
  family: ['wife', 'kids', 'dog']
};

const summary = {...defaultPerson, ...me};

/*
{  
   "name":"David Walsh",
   "gender":"Male",
   "hair":{  
      "cut":"short"
   },
   "eyes":"blue",
   "family":[  
      "wife",
      "kids",
      "dog"
   ]
}
*/

In the sample above, you'll notice that the hair object's color is gone instead of merged because the spread operator simply keeps the last provided values, which in this case is me.hair.  The same merge problem applies to arrays -- you'll notice mom and dad aren't merged from the defaultPerson object's family array.  Yikes!

Deep merging in JavaScript is important, especially with the common practice of "default" or "options" objects with many properties and nested objects that often get merged with instance-specific values.  If you're looking for a utility to help with deep merges, look no further than the tiny deepmerge utility!

When you use the deepmerge utility, you can recursively merge any number of objects (including arrays) into one final object.  Let's take a look!

const deepmerge = require('deepmerge');

// ...

const summary = deepmerge(defaultPerson, me);

/*
{  
   "name":"David Walsh",
   "gender":"Male",
   "hair":{  
      "color":"brown",
      "cut":"short"
   },
   "eyes":"blue",
   "family":[  
      "mom",
      "dad",
      "wife",
      "kids",
      "dog"
   ]
}
*/

deepmerge can handle much more complicated merges: nested objects and deepmerge.all to merge more than two objects:

const result = deepmerge.all([,
  { level1: { level2: { name: 'David', parts: ['head', 'shoulders'] } } },
  { level1: { level2: { face: 'meh', parts: ['knees', 'toes'] } } },
  { level1: { level2: { eyes: 'more meh', parts: ['eyes'] } } },
]);

/*
{  
   "level1":{  
      "level2":{  
         "name":"David",
         "parts":[  
            "head",
            "shoulders",
            "knees",
            "toes",
            "eyes"
         ],
         "face":"meh",
         "eyes":"more meh"
      }
   }
}
*/

deepmerge is an amazing utility is a relatively small amount of code:

function isMergeableObject(val) {
    var nonNullObject = val && typeof val === 'object'

    return nonNullObject
        && Object.prototype.toString.call(val) !== '[object RegExp]'
        && Object.prototype.toString.call(val) !== '[object Date]'
}

function emptyTarget(val) {
    return Array.isArray(val) ? [] : {}
}

function cloneIfNecessary(value, optionsArgument) {
    var clone = optionsArgument && optionsArgument.clone === true
    return (clone && isMergeableObject(value)) ? deepmerge(emptyTarget(value), value, optionsArgument) : value
}

function defaultArrayMerge(target, source, optionsArgument) {
    var destination = target.slice()
    source.forEach(function(e, i) {
        if (typeof destination[i] === 'undefined') {
            destination[i] = cloneIfNecessary(e, optionsArgument)
        } else if (isMergeableObject(e)) {
            destination[i] = deepmerge(target[i], e, optionsArgument)
        } else if (target.indexOf(e) === -1) {
            destination.push(cloneIfNecessary(e, optionsArgument))
        }
    })
    return destination
}

function mergeObject(target, source, optionsArgument) {
    var destination = {}
    if (isMergeableObject(target)) {
        Object.keys(target).forEach(function (key) {
            destination[key] = cloneIfNecessary(target[key], optionsArgument)
        })
    }
    Object.keys(source).forEach(function (key) {
        if (!isMergeableObject(source[key]) || !target[key]) {
            destination[key] = cloneIfNecessary(source[key], optionsArgument)
        } else {
            destination[key] = deepmerge(target[key], source[key], optionsArgument)
        }
    })
    return destination
}

function deepmerge(target, source, optionsArgument) {
    var array = Array.isArray(source);
    var options = optionsArgument || { arrayMerge: defaultArrayMerge }
    var arrayMerge = options.arrayMerge || defaultArrayMerge

    if (array) {
        return Array.isArray(target) ? arrayMerge(target, source, optionsArgument) : cloneIfNecessary(source, optionsArgument)
    } else {
        return mergeObject(target, source, optionsArgument)
    }
}

deepmerge.all = function deepmergeAll(array, optionsArgument) {
    if (!Array.isArray(array) || array.length < 2) {
        throw new Error('first argument should be an array with at least two elements')
    }

    // we are sure there are at least 2 values, so it is safe to have no initial value
    return array.reduce(function(prev, next) {
        return deepmerge(prev, next, optionsArgument)
    })
}

Little code with big functionality?  That's my favorite type of utility!  deepmerge is used all over the web and for good reason!

Recent Features

  • By
    An Interview with Eric Meyer

    Your early CSS books were instrumental in pushing my love for front end technologies. What was it about CSS that you fell in love with and drove you to write about it? At first blush, it was the simplicity of it as compared to the table-and-spacer...

  • By
    9 Mind-Blowing WebGL Demos

    As much as developers now loathe Flash, we're still playing a bit of catch up to natively duplicate the animation capabilities that Adobe's old technology provided us.  Of course we have canvas, an awesome technology, one which I highlighted 9 mind-blowing demos.  Another technology available...

Incredible Demos

  • By
    Translate Content with the Google Translate API and JavaScript

    Note:  For this tutorial, I'm using version1 of the Google Translate API.  A newer REST-based version is available. In an ideal world, all websites would have a feature that allowed the user to translate a website into their native language (or even more ideally, translation would be...

  • By
    Create a Clearable TextBox with the Dojo Toolkit

    Usability is a key feature when creating user interfaces;  it's all in the details.  I was recently using my iPhone and it dawned on my how awesome the "x" icon is in its input elements.  No holding the delete key down.  No pressing it a...

Discussion

  1. deepmerge suffers exact same issue Object.assign does.

    We are not in ES3 times anymore, developers should start learning how to really merge, copy, or clone objects.

    I’ve explained that here:
    https://www.webreflection.co.uk/blog/2015/10/06/how-to-copy-objects-in-javascript

    And I’ve created a library that is as small but without surprises.
    https://github.com/WebReflection/cloner

  2. let obj = {a: {b: {c: false, other:''}, other: ''}, other: ''};
    // making obj.a.b.c = true
    
    let newObj = Object.assign(obj, {
      a: Object.assign(obj.a, {
        b: Object.assign(obj.a.b, {c: true})
      })
    });
    
  3. divp

    When two or more object arguments are supplied to $.extend(), properties from all of the objects are added to the target object. Arguments that are null or undefined are ignored.

    If only one argument is supplied to $.extend(), this means the target argument was omitted. In this case, the jQuery object itself is assumed to be the target. By doing this, you can add new functions to the jQuery namespace. This can be useful for plugin authors wishing to add new methods to jQuery.

    Thanks for sharing in-depth knowledge…nice article

  4. Hi, thanks for the article
    I’m a Scala developer, so JS is pretty messy for me :( But a such posts like this one is really helpful in learning JavaScript

  5. Zoë

    Thank you for posting this! Very helpful, as I am trying to reduce my code’s dependency on outside libraries, if I can instead create a utility function for the specific function I need. :D

  6. The only thing I really disliked in this small lib is that it favors the left element instead the latter, so it doesn’t comply with what spread operators do…

  7. Ez

    amazing, thank you so much!!! i’m going going down a weird wormhole of ultra DRY code and this was driving me crazy for… a while haha.

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!