在时间属性上慎用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”这个函数。