38

Normally to shallow copy objects I would use angular.extend()

Here's an example of that:

var object1 = {
  "key": "abc123def456",
  "message": {
    "subject": "Has a Question",
    "from": "example1@example.com",
    "to": "example2@example.com"
   }
};

var object2 = {
  "key": "00700916391"
};

console.log(angular.extend({}, object1, object2));

Would give us:

{
 "key": "00700916391",
 "message": {
   "subject": "Has a Question",
   "from": "example1@example.com",
   "to": "example2@example.com"
  }
}

But what if I wanted to merge objects so that parent keys are not over written by child objects:

var object1 = {
  "key": "abc123def456",
  "message": {
    "subject": "Has a Question",
    "from": "example1@example.com",
    "to": "example2@example.com"
   }
};

var object2 = {
  "key": "00700916391",              //Overwrite me
  "message": {                       //Dont overwrite me!
    "subject": "Hey what's up?",     //Overwrite me
    "something": "something new"     //Add me
   }
};

console.log(merge(object1, object2));

Would give us:

{
 "key": "00700916391",
 "message": {
   "subject": "Hey what's up?",
   "from": "example1@example.com",
   "to": "example2@example.com",
   "something": "something new"
  }
}
  • Is there an Angular function that already does a deep merge that I am not aware of?

  • If not is there a native way to do this in javascript recursively for n levels deep?

Dan Kanze
  • 18,485
  • 28
  • 81
  • 134

5 Answers5

39

Angular 1.4 or later

Use angular.merge:

Unlike extend(), merge() recursively descends into object properties of source objects, performing a deep copy.

angular.merge(object1, object2); // merge object 2 into object 1

Older versions of Angular:

There is no reason a simple recursive algorithm shouldn't work :)

Assuming they're both the result of JSON.stringify or similar:

function merge(obj1,obj2){ // Our merge function
    var result = {}; // return result
    for(var i in obj1){      // for every property in obj1 
        if((i in obj2) && (typeof obj1[i] === "object") && (i !== null)){
            result[i] = merge(obj1[i],obj2[i]); // if it's an object, merge   
        }else{
           result[i] = obj1[i]; // add it to result
        }
    }
    for(i in obj2){ // add the remaining properties from object 2
        if(i in result){ //conflict
            continue;
        }
        result[i] = obj2[i];
    }
    return result;
}

Here is a working fiddle

(Note, arrays are not handled here)

MKroeders
  • 7,562
  • 4
  • 24
  • 39
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Now, what would be really cool, is if you could pass `n` objects as paramters. Similarly to how `angular.extend()` works... Up to the challenge ? :) – Dan Kanze Jun 22 '13 at 00:06
  • @DanKanze Instead of looping through `obj2` in the second `for... in`, wrap that in another loop through `arguments` (don't start the index at 0, you want to skip the first object :) (The reason it's not a part of the answer, is the same reason why there is no depth limit or array treatment - it complicates a simple answer with specifics) If you have trouble implementing this - you're welcome to discuss this with me in the JS room. – Benjamin Gruenbaum Jun 22 '13 at 00:10
  • You omit a `var` declaration for `i` - `var result = {}, i;` – hooblei May 06 '14 at 11:47
  • @hooblei thanks for noticing, I make a such mistakes when writing code in a jshint-less environment like the SO markdown editor all the time. I've fixed it - nice catch. – Benjamin Gruenbaum May 06 '14 at 12:04
  • I think this merge function is no real replacement for the angular.merge function in 1.4. The aim of merging is to deeply extend an object. Your solution creates a new object (result) and obj1 isn't changed in any way. – kwrl Aug 06 '15 at 13:51
  • @TonyO'Hagan what's `typeof null`? – Benjamin Gruenbaum Sep 11 '15 at 18:53
6

In the new version of Angularjs they added merge function which will perform the deep copy.

For the older versions, I have created my custom function by copying the code of merge function from new version of Angularjs. Below is the code for the same,

function merge(dst){
  var slice = [].slice;
  var isArray = Array.isArray;
  function baseExtend(dst, objs, deep) {
    for (var i = 0, ii = objs.length; i < ii; ++i) {
      var obj = objs[i];
      if (!angular.isObject(obj) && !angular.isFunction(obj)) continue;
      var keys = Object.keys(obj);
      for (var j = 0, jj = keys.length; j < jj; j++) {
        var key = keys[j];
        var src = obj[key];
        if (deep && angular.isObject(src)) {
          if (!angular.isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
          baseExtend(dst[key], [src], true);
        } else {
          dst[key] = src;
        }
      }
    }

    return dst;
  }
  return baseExtend(dst, slice.call(arguments, 1), true);
}

Hope this will help someone who is wondering why angular.merge is not working in older versions.

Mathankumar
  • 627
  • 1
  • 9
  • 19
  • 1
    I've upvoted you as this is the best answer as it uses the a modified version of the angular code. Perhaps you could add a line like "if(angular.hasOwnProperty('merge'))return angular.merge.apply(angular, arguments);". This would mean that the native Angular version is used if available and the polyfill version when using <1.4. – Stephen Simpson May 14 '15 at 10:09
  • 1
    Almost ... what I finally coded was: if (!angular.hasOwnProperty('merge')) { angular.merge = function(dst) { ... } } – Tony O'Hagan Sep 07 '15 at 13:32
  • 1
    Watch out for this 1.4 bug in baseExtend() ... http://geekswithblogs.net/shaunxu/archive/2015/05/29/pay-attention-to-quotangular.mergequot-in-1.4-on-date-properties.aspx that treats Dates() as objects and incorrectly attempts to merge them – Tony O'Hagan Sep 07 '15 at 13:46
4

angular.merge polyfill for angular < 1.4.0

if (!angular.merge) {
  angular.merge = (function mergePollyfill() {
    function setHashKey(obj, h) {
      if (h) {
        obj.$$hashKey = h;
      } else {
        delete obj.$$hashKey;
      }
    }

    function baseExtend(dst, objs, deep) {
      var h = dst.$$hashKey;

      for (var i = 0, ii = objs.length; i < ii; ++i) {
        var obj = objs[i];
        if (!angular.isObject(obj) && !angular.isFunction(obj)) continue;
        var keys = Object.keys(obj);
        for (var j = 0, jj = keys.length; j < jj; j++) {
          var key = keys[j];
          var src = obj[key];

          if (deep && angular.isObject(src)) {
            if (angular.isDate(src)) {
              dst[key] = new Date(src.valueOf());
            } else {
              if (!angular.isObject(dst[key])) dst[key] = angular.isArray(src) ? [] : {};
              baseExtend(dst[key], [src], true);
            }
          } else {
            dst[key] = src;
          }
        }
      }

      setHashKey(dst, h);
      return dst;
    }

    return function merge(dst) {
      return baseExtend(dst, [].slice.call(arguments, 1), true);
    }
  })();
}
Luckylooke
  • 4,061
  • 4
  • 36
  • 49
3

here is a solution that handels multiple objects merge (more than two objects):

Here is an extendDeep function based off of the angular.extend function. If you add this to your $scope, you would then be able to call

$scope.meta = $scope.extendDeep(ajaxResponse1.myMeta, ajaxResponse2.defaultMeta);

and get the answer you are looking for.

$scope.extendDeep = function extendDeep(dst) {
  angular.forEach(arguments, function(obj) {
    if (obj !== dst) {
      angular.forEach(obj, function(value, key) {
        if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
          extendDeep(dst[key], value);
        } else {
          dst[key] = value;
        }     
      });   
    }
  });
  return dst;
};
Community
  • 1
  • 1
Ali
  • 6,808
  • 3
  • 37
  • 47
1

If you're using < 1.4

You can use lodash's built in _.merge() that does the same thing as angular > 1.4's version

Saved me from writing new functions since lodash is pretty popular with angular folks already

ObjectiveTruth
  • 878
  • 10
  • 17