使用AngularJS进行对象深度合并

38

通常我会使用angular.extend()来浅复制对象。

以下是一个例子:

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));

会给我们:

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

但如果我想合并对象,使得父键不被子对象覆盖,该怎么办:

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));

会给我们:

{
 "key": "00700916391",
 "message": {
   "subject": "Hey what's up?",
   "from": "example1@example.com",
   "to": "example2@example.com",
   "something": "something new"
  }
}
  • 我是否不知道的有一个Angular函数可以进行深度合并?

  • 如果没有,那么是否有一种递归地处理深层级javascript对象的本地方法来完成此操作?

5个回答

39

Angular 1.4或更高版本

使用angular.merge

merge()extend()不同,会递归地深复制源对象的属性。

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

旧版本 Angular:

没有理由一个简单的递归算法不应该起作用 :)

假设它们都是 JSON.stringify 或类似方法的结果:

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;
}

这里是一个可运行的 fiddle

(注意,这里没有处理数组)


现在,真正酷的事情是,如果您可以将n个对象作为参数传递。就像angular.extend()的工作原理一样...准备好迎接挑战了吗? :) - Dan Kanze
@DanKanze 在第二个 for... in 循环中,不要遍历 obj2,而是通过另一个循环遍历 arguments(不要从0开始索引,你需要跳过第一个对象 :) (它不是答案的一部分的原因与没有深度限制或数组处理的原因相同-它会使简单的答案变得复杂)如果你在实现这个过程中遇到问题-欢迎在 JS 房间与我讨论。 - Benjamin Gruenbaum
你忘记为 i 声明一个 var - var result = {}, i; - hooblei
@hooblei 谢谢你的注意,我在像 SO markdown 编辑器这样没有 jshint 的环境下编写代码时经常会犯这样的错误。我已经修复了它 - 很好的发现。 - Benjamin Gruenbaum
我认为这个合并函数并不能真正替代1.4版中的angular.merge函数。合并的目的是深度扩展一个对象。你的解决方案创建了一个新对象(result),而obj1没有任何改变。 - kwrl
@TonyO'Hagan typeof null 是什么? - Benjamin Gruenbaum

6
在Angularjs的新版本中,他们添加了merge函数,该函数将执行深度复制。
对于旧版本,我已通过复制来自新版本的Angularjs的merge函数的代码创建了自定义函数。下面是相同的代码:
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);
}

希望这篇文章能够帮助那些想知道为什么在旧版本中angular.merge无法正常工作的人。

1
我已经给你点赞,因为这是最好的答案,它使用了修改过的 Angular 代码。也许你可以添加一行代码,像这样:"if(angular.hasOwnProperty('merge'))return angular.merge.apply(angular, arguments);"。这将意味着如果可用,则使用原生的 Angular 版本,而在使用<1.4版本时则使用 polyfill 版本。 - Stephen Simpson
1
几乎...最终我编写的代码是: if (!angular.hasOwnProperty('merge')) { angular.merge = function(dst) { ... } } - Tony O'Hagan
1
请注意在baseExtend()中的1.4版本bug... http://geekswithblogs.net/shaunxu/archive/2015/05/29/pay-attention-to-quotangular.mergequot-in-1.4-on-date-properties.aspx,该版本将Dates()视为对象并错误地尝试合并它们。 - Tony O'Hagan

4

针对 Angular 1.4.0 以前的版本提供的 angular.merge 兼容补丁。

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);
    }
  })();
}

3

这里提供了一个处理多个对象合并(超过两个对象)的解决方案:

以下是基于angular.extend函数的extendDeep函数。如果您将此添加到您的$scope中,则可以调用

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

并获得您寻找的答案。
$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;
};

1
PS:此函数具有副作用,会将后面的参数值复制到前面的参数中。 - mayankcpdixit

1
如果您使用的是小于1.4版本的话,您可以使用lodash内置的_.merge(),它和大于1.4版本的angular的版本做的是同样的事情。这使我免于编写新函数,因为lodash已经在angular社区中相当流行了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接