Shaun Xu

The Sheep-Pen of the Shaun


News

logo

Shaun, the author of this blog is a semi-geek, clumsy developer, passionate speaker and incapable architect with about 10 years’ experience in .NET and JavaScript. He hopes to prove that software development is art rather than manufacturing. He's into cloud computing platform and technologies (Windows Azure, Amazon and Aliyun) and right now, Shaun is being attracted by JavaScript (Angular.js and Node.js) and he likes it.

Shaun is working at Worktile Inc. as the chief architect for overall design and develop worktile, a web-based collaboration and task management tool, and lesschat, a real-time communication aggregation tool.

MVP

My Stats

  • Posts - 122
  • Comments - 588
  • Trackbacks - 0

Tag Cloud


Recent Comments


Recent Posts


Archives


Post Categories


Image Galleries


.NET



Today when I upgraded my application from Angular.js 1.3.9 to the latest 1.4, I got some bugs. After investigated a bit I found they are related with date properties and "angular.merge" function, which was included in 1.4.

In the official document, "angular.merge" is

Deeply extends the destination object "dst" by copying own enumerable properties from the "src" object(s) to "dst".

It also mentioned the different with "angular.extend", which had been included in previous versions.

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

 

Let's have a look on a very simple example. In code below I have a source object and custom object defined in scope. And I used "angular.extend" to copy custom object into source.

   1: $scope.source = {
   2:   name: 'Shaun',
   3:   age: 35,
   4:   birthDate: new Date(1997, 5, 27),
   5:   skills: {
   6:     dotNet: {
   7:       level: 'expert',
   8:       years: 10
   9:     },
  10:     javaScript: {
  11:       level: 'newbie',
  12:       years: 2
  13:     }
  14:   },
  15:   mvp: [
  16:     2011,
  17:     2012,
  18:     2013,
  19:     2014,
  20:     2015
  21:   ]
  22: };
  23:  
  24: $scope.custom = {
  25:   name: 'Ziyan',
  26:   age: 35,
  27:   skills: {
  28:     dotNet: {
  29:       level: 'hero', 
  30:       years: 100,
  31:       active: true
  32:     },
  33:   },
  34:   mvp: [
  35:     2016
  36:   ]
  37: };
  38:  
  39: $scope.extend = angular.extend({}, $scope.source, $scope.custom);

From the result we can see, since "angular.extend" performs shallow copy, primitive value property such as "name", "age" and "bitrhDate" are merged. But since "skills" and "mvp" are object and array, "angular.extend" will just copy the entire value, rather than their members.

image

 

Now let's using "angular.merge".

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

Now we can see, when using "angular.merge", it will copy object properties recursively.

image

 

Everything looks great till now. But someone may find when using "angular.merge", one of the property, "birthDate" was been set as am empty object.

image

If we deep into the source code of Angular.js we will find, both "angular.extend" and "angular.merge" are invoking an internal function named "baseExtend". It merges objects to destination object, with a flag parameter indicates whether it's a deep merge or not.

Inside this function, it loops each enumerable properties, try to copy the value to destination object. If the source property is an object and need to be deeply copied, Angular.js will create an empty object in destination and perform this function against this property recursively.

   1: function baseExtend(dst, objs, deep) {
   2:   var h = dst.$$hashKey;
   3:  
   4:   for (var i = 0, ii = objs.length; i < ii; ++i) {
   5:     var obj = objs[i];
   6:     if (!isObject(obj) && !isFunction(obj)) continue;
   7:     var keys = Object.keys(obj);
   8:     for (var j = 0, jj = keys.length; j < jj; j++) {
   9:       var key = keys[j];
  10:       var src = obj[key];
  11:  
  12:       if (deep && isObject(src)) {
  13:         if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
  14:         baseExtend(dst[key], [src], true);
  15:       } else {
  16:         dst[key] = src;
  17:       }
  18:     }
  19:   }
  20:  
  21:   setHashKey(dst, h);
  22:   return dst;
  23: }

It works in almost all cases but Date. If we have a Date property defined, for example "birthDate", it will check if this property is an object by using "angular.isObject" and it will return "true". So it will create a property named "birthDate" in destination with an empty object, and invoke "baseExtend" against "birthDate". But since "birthDate" is a Date which is no enumerable property, so it will not assign any data. This is the reason we found in result, "birthDate" property is empty.

If I copied Angular.js "baseExtend" function to my scope, and changed the code as below, which will perform simple copy when the property is Date, it will work.

   1: $scope.$baseExtend = function (dst, objs, deep) {
   2:   for (var i = 0, ii = objs.length; i < ii; ++i) {
   3:     var obj = objs[i];
   4:     if (!angular.isObject(obj) && !angular.isFunction(obj)) {
   5:       console.log('[' + obj + '] = (skip)');
   6:       continue;
   7:     }
   8:     var keys = Object.keys(obj);
   9:     for (var j = 0, jj = keys.length; j < jj; j++) {
  10:       var key = keys[j];
  11:       var src = obj[key];
  12:       // perform deep copy if
  13:       // 1. spcified by user
  14:       // 2. source property is an object
  15:       // 3. source property is NOT a date
  16:       if (deep && angular.isObject(src) && !angular.isDate(src)) {
  17:         if (!angular.isObject(dst[key])) {
  18:           console.log('[' + key + '] = (Try copy an object to an non-object, create an empty for deep copy.)');
  19:           dst[key] = angular.isArray(src) ? [] : {};
  20:         }
  21:         $scope.$baseExtend(dst[key], [src], true);
  22:       } 
  23:       else {
  24:         dst[key] = src;
  25:         
  26:         console.log('[' + key + '] = ' + src);
  27:       }
  28:     }
  29:   }
  30:   return dst;
  31: };
  32:  
  33: $scope.sample = $scope.$baseExtend({}, [$scope.source, $scope.custom], true);

This is the result.

image

 

Summary

Upgrade framework to a new version is always be an adventure. We need to perform a lot of regression tests to make sure it will not break anything. This problem was found when I perform E2E tests.

I think this is bug in Angular.js and I had posted an issue. But before their verification and response, I think you should pay more attention when using "angular.merge" function in your application.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.

Comments

Gravatar # re: Pay Attention to "angular.merge" in 1.4 on Date Properties
Posted by Roberta on 7/2/2015 11:35 PM
I must admit that although I have enough experience, there are details that don't draw my attention at the moment, which is why sometimes I get in the position to be blocked by some internal error. Then I turn to the advice of people far more advanced than me. Thanks for the tip!
Gravatar # re: Pay Attention to "angular.merge" in 1.4 on Date Properties
Posted by saravana on 9/1/2015 10:40 AM
this is a very helpful post... thanks a lot...
Post A Comment
Title:
Name:
Email:
Comment:
Verification: