在时间属性上慎用Angular.js1.4的“angular.merge”函数

原文:Pay Attention to "angular.merge" in 1.4 on Date Properties

今天我将应用从Angular.js1.3.9升级到1.4的时候,发现了一些Bug。在经过一翻研究后,发现它们与时间属性及1.4中的“angular.merge”函数有关。

在官方文档中,“angular.merge”的表述如下:

复制src对象中的可枚举属性到dst对象中,以深度扩展目标对象“dst”。

同时也提到了与先前版本中的”angular.extend“的区别。

与"extend()"不同,在进行深度复制时,"merge()"支持递归复制。

让我们来看一个简单的例子,在下面的代码中,作用域内有一个源对象和一个自定义对象,接着我用”angular.extend“将自定义对象复制到源对象中。

 $scope.source = {
   name: 'Shaun',
   age: 35,
   birthDate: new Date(1997, 5, 27),
   skills: {
     dotNet: {
       level: 'expert',
       years: 10
     },
     javaScript: {
       level: 'newbie',
       years: 2
     }
   },
   mvp: [
     2011,
     2012,
     2013,
     2014,
     2015
   ]
 };
  
 $scope.custom = {
   name: 'Ziyan',
   age: 35,
   skills: {
     dotNet: {
       level: 'hero', 
       years: 100,
       active: true
     },
   },
   mvp: [
     2016
   ]
 };
  
 $scope.extend = angular.extend({}, $scope.source, $scope.custom);

从结果中我们可以看到,“angular.extend”执行的是浅拷贝,像“name”,”age”和“birthDate”这些原始属性值已经合并了,但因为“skills”和”mvp”是对象和数组,”angular.extend“会复制全部值,而不是数组或对象中的成员。

这里写图片描述

现在让我们用“angular.merge”来复制:

$scope.merge = angular.merge({}, $scope.source, $scope.custom);

我们可能看到,当用“angular.merge”的时候,对象属性是递归复制的。

这里写图片描述

到目前为止,一切看起来还不错。但是有些人可能会发现,在使用“angular.merge”的时候,其中一个属性“birthDate”被设成了空对象。

这里写图片描述

如果深入研究angular.js的源码,我们会发现,无论是“angular.extend”还是“angular.merge”都调用了“baseExtend”这个内部函数。它将多个对象合并到目标对象,用一个标志参数来表明是否为深度合并。

在这个函数内部,会遍历每个可枚举属性,将值复制到目标对象。如果源属性是一个对象并且需要深度复制,Angular.js会在目标对象中创建一个空对象,并且对这个属性递归执行这个函数。

   function baseExtend(dst, objs, deep) {
      var h = dst.$$hashKey;
     
      for (var i = 0, ii = objs.length; i < ii; ++i) {
        var obj = objs[i];
        if (!isObject(obj) && !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 && isObject(src)) {
           if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
           baseExtend(dst[key], [src], true);
         } else {
           dst[key] = src;
         }
       }
     }
    
     setHashKey(dst, h);
     return dst;
   }

除了日期之外,它几乎可以处理所有情况。如果我们定义了一个日期属性,例如“birthDate”,它就会用“angular.isObject”检查这个属性是否是为对象,返回的是“true”。所以它会在目标对象中创建一个属性名为“birthDate”的空对象,并且对“birthDate”调用“baseExtend”。但是因为“birthDate”的属性为Date,并不是可枚举属性,所以这个对象不会分配到任何数据。这就是为什么结果中"birthDate"属性为空的原因。

如果我将Angular.js 中的“baseExtend”函数复制到作用域内,并将代码改成如下形式,当属性为Date时,也会执行简单的复制了。

 $scope.$baseExtend = function (dst, objs, deep) {
   for (var i = 0, ii = objs.length; i < ii; ++i) {
     var obj = objs[i];
     if (!angular.isObject(obj) && !angular.isFunction(obj)) {
      console.log('[' + obj + '] = (skip)');
       continue;
     }
     var keys = Object.keys(obj);
     for (var j = 0, jj = keys.length; j < jj; j++) {
       var key = keys[j];
       var src = obj[key];
       // perform deep copy if
       // 1. spcified by user
       // 2. source property is an object
       // 3. source property is NOT a date
       if (deep && angular.isObject(src) && !angular.isDate(src)) {
         if (!angular.isObject(dst[key])) {
           console.log('[' + key + '] = (Try copy an object to an non-object, create an empty for deep copy.)');
           dst[key] = angular.isArray(src) ? [] : {};
         }
         $scope.$baseExtend(dst[key], [src], true);
       } 
       else {
         dst[key] = src;
         
         console.log('[' + key + '] = ' + src);
       }
     }
   }
   return dst;
 };
  
 $scope.sample = $scope.$baseExtend({}, [$scope.source, $scope.custom], true);

结果如下:

这里写图片描述

总结:

将框架升级到新版本是一种冒险行为。我们需要进行大量的回归测试,以确保不会出现任何问题。我在执行E2E测试时发现了这个问题。我想这是Angular.js的Bug,我已经将这个问题贴了出来。在他们确认和修改之前,在你的应用中,慎用“angular.merge”这个函数。


对角另一面
1.4k 声望79 粉丝

踏在路上交叉点,你在对角另一面。