Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

fix(copy): preserve prototype chain when copying object #5063

Conversation

gentooboontoo
Copy link
Contributor

So far, angular.copy was copying all properties including those from
prototype chain and was losing the whole prototype chain (except for Date,
Regexp, and Array).

Deep copy should exclude properties from the prototype chain because it
is useless to do so. When modified, properties from prototype chain are
overwritten on the object itself and will be deeply copied then.

Moreover, preserving prototype chain allows instanceof operator to be
consistent between the source object and the copy.
Before this change,

var Foo = function() {};
var foo = new Foo();
var fooCopy = angular.copy(foo);
foo instanceof Foo; // => true
fooCopy instanceof Foo; // => false

Now,

foo instanceof Foo; // => true
fooCopy instanceof Foo; // => true

The new behaviour is useful when using $http transformResponse. When
receiving JSON data, we could transform it and instantiate real object
"types" from it. The transformed response is always copied by Angular.
The old behaviour was losing the whole prototype chain and broke all
"types" from third-party libraries depending on instanceof.

This PR is somewhat related to issues #4996, #3767 and #1427.

@petebacondarwin
Copy link
Contributor

Can you provide an explicit example of the use case that motivates this change?

@gentooboontoo
Copy link
Contributor Author

I have instances with "typed" properties I need to persist with JSON format.

For instance, I could have the following object whose one of its properties is an instance of Qty from my library gentooboontoo/js-quantities (but it could be an instance from moment.js library):

var record = {
    ...
    volume: new Qty('3 l')
    ...
};

record.volume instanceof Qty; // => true

This record is persisted in JSON with its type inlined because the property value is not JSON native type:

{
    ...
    "volume": {
      "value": "3 l",
      "type": "quantity"
    }
    ...
};

I choose to override default transformResponse from $resource to deserialize this record and properly instantiate record properties (I hope it is the good way to do it).

transformResponse returns the original deserialized version of the persisted record as expected:

var recordFromTransformResponse = {
    ...
    volume: new Qty('3 l')
    ...
};

recordFromTransformResponse.volume instanceof Qty; // => true

Unfortunately, Angular resource make a copy of that deserialized object and the final object retrieved is:

var recordFromResource = Record.get(...);
...
// Surprisingly it is false
recordFromResource.volume instanceof Qty; // => false

Some code on my side needs properties to respond correctly to instanceof operator (whether or not the record has just been created by the app or received through $resource).

angular.copy removes the prototype chain of all nested properties not being a native type and copy every properties from the whole prototype chain on the object itself. I don't see any usefulness even with deep copy. If source object has "shared" properties through its prototype chain, its deep copy should share these properties through its prototype chain too (I could be wrong but it is still a real deep copy).

Am I wrong in my reasoning?

@IgorMinar
Copy link
Contributor

I'm sorry, but I wasn't able to verify your CLA signature. CLA signature is required for any code contributions to AngularJS.

Please sign our CLA and ensure that the CLA signature email address and the email address in this PR's commits match.

If you signed the CLA as a corporation, please let me know the company's name.

Thanks a bunch!

PS: If you signed the CLA in the past then most likely the email addresses don't match. Please sign the CLA again or update the email address in the commit of this PR.
PS2: If you are a Googler, please sign the CLA as well to simplify the CLA verification process.

@gentooboontoo
Copy link
Contributor Author

I have just fixed the e-mail of all PR commits and I have signed the CLA with the right e-mail.

@gentooboontoo
Copy link
Contributor Author

I have rebased the commits onto master and refactored them in two:

  • the first one related to the feature
  • a second one for IE8 polyfill (to be fixed or dropped depending on the target milestone if PR is accepted)

@petebacondarwin, I have also unpublished changes (based on this PR) to copy getters and setters that could fix #5085 but I not sure if they should be integrated here.

@gentooboontoo
Copy link
Contributor Author

As Angular 1.3 discontinues IE8 and as I do not expect this PR to land on Angular 1.2x branch, I will remove failing IE8 tests in the very near future.

@gentooboontoo
Copy link
Contributor Author

I have rebased the PR onto master and I have dropped IE8 support. All tests pass now.

@@ -770,7 +770,7 @@ function copy(source, destination){
} else if (isRegExp(source)) {
destination = new RegExp(source.source);
} else if (isObject(source)) {
destination = copy(source, {});
destination = copy(source, Object.create(Object.getPrototypeOf(source)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line means that backporting this to 1.2.x is impossible or difficult.

I think we could get around this, though, using a strategy similar to what we have in the injector:

function Constructor() {}
Constructor.prototype = isFunction(source.constructor)
                      ? source.constructor.prototype
                      : Object.prototype;
destination = copy(source, new Constructor());

I'm not positive that would work, but I think it would. It might be a bit expensive, though.

@gentooboontoo
Copy link
Contributor Author

Thank you very much @caitp for your comment. In the first version of this PR, I tried to support IE<=8 by creating the following shim function to support Object.create into Angular.js:

/**
 * Creates object through Object.create or polyfill for IE <= 8
 *
 * @private
 * @param {Object} proto - The object representing the prototype of the new object to create
 * @returns {Object} Newly created object with proto as prototype
 * */
var createObject = (function () {
  function Constructor() {}
  if(typeof Object.create === "function") {
    return Object.create;
  }
  else { // msie <= 8
    return function createObject(proto) {
      Constructor.prototype = proto;
      return new Constructor();
    };
  }
}());

As you can see, createObject is defined through an IIFE so function Constructor() is only declared once and typeof Object.create === "function" only evaluated once.
Then we just have to replace Object.create calls with createObject instead:

destination = copy(source, createObject(Object.getPrototypeOf(source)));

This way, it should not be too expensive. If needed, I could make a separate PR for this or try to re-include it here.

@gentooboontoo
Copy link
Contributor Author

I have realized that there's also a problem for Object.getPrototypeOf too with IE8.

As a side note, I didn't expect this PR to land to 1.2.x branch. I found these shims too overkill just to support IE8 with 1.2.x. But if needed I could try to fix them.

@caitp
Copy link
Contributor

caitp commented Apr 10, 2014

You can get the prototype of the constructor via the cobstructor property, if one exists. John Resig shows a polyfill at http://ejohn.org/blog/objectgetprototypeof/, which despite the caveats, should generally work "well enough'

@shaneneuerburg
Copy link

Thanks for the fix @gentooboontoo. This resolved an issue I was having using moment.js with angular. Hopefully the PR gets merged

@leider
Copy link

leider commented May 27, 2014

Hi, I am having the same issues with moment.js. I would highly appreciate integrating it. Can 100% confirm @shaneneuerburg It fixed my issue as well

@etodanik
Copy link

I'm also having trouble with that same momentjs issue. This would be pretty handy. From what I gather this isn't compatible with the latest 1.2.x branch? Would anyone know the way to adapt it?

@gentooboontoo
Copy link
Contributor Author

@caitp @petebacondarwin I have rebased the PR onto master branch (the PR was outdated by some changes). I think this PR targets v1.3.x branch although it works with 1.2.x branch and non IE8 browsers.

If needed, I could re-add IE8 support I removed earlier (and fixed according to previous @caitp's comment) but it would be a little bit ugly as v1.3 is approaching.

@etodanik
Copy link

I applied this patch and it solved a very annoying problem with $watch on
moment.js.

It would be great to see this merged.

On Tue, Jun 10, 2014 at 7:25 PM, Julien Sanchez notifications@github.com
wrote:

@caitp https://github.com/caitp @petebacondarwin
https://github.com/petebacondarwin I have rebased the PR onto master
branch (the PR was outdated by some changes). I think this PR targets
v1.3.x branch although it works with 1.2.x branch and non IE8 browsers.

If needed, I could re-add IE8 support I removed earlier (and fixed
according to previous @caitp https://github.com/caitp's comment) but it
would be a little bit ugly as v1.3 is approaching.


Reply to this email directly or view it on GitHub
#5063 (comment).

@myitcv
Copy link
Contributor

myitcv commented Jun 16, 2014

+1 (given #7846)

@myitcv
Copy link
Contributor

myitcv commented Jun 27, 2014

Are there plans to include this in v1.3?

@gentooboontoo gentooboontoo changed the title Preserve prototype chaining when copying object Preserve prototype chain when copying object Jun 27, 2014
@gentooboontoo gentooboontoo changed the title Preserve prototype chain when copying object fix(copy): preserve prototype chain when copying object Jun 27, 2014
@petebacondarwin
Copy link
Contributor

@lgalfaso - do you have a view on this? It seems like a reasonable change.

So far, angular.copy was copying all properties including those from
prototype chain and was losing the whole prototype chain (except for Date,
Regexp, and Array).

Deep copy should exclude properties from the prototype chain because it
is useless to do so. When modified, properties from prototype chain are
overwritten on the object itself and will be deeply copied then.

Moreover, preserving prototype chain allows instanceof operator to be
consistent between the source object and the copy.
Before this change,

    var Foo = function() {};
    var foo = new Foo();
    var fooCopy = angular.copy(foo);
    foo instanceof Foo; // => true
    fooCopy instanceof Foo; // => false

Now,

    foo instanceof Foo; // => true
    fooCopy instanceof Foo; // => true

The new behaviour is useful when using $http transformResponse. When
receiving JSON data, we could transform it and instantiate real object
"types" from it. The transformed response is always copied by Angular.
The old behaviour was losing the whole prototype chain and broke all
"types" from third-party libraries depending on instanceof.
@gentooboontoo
Copy link
Contributor Author

Just rebased onto master.

@IgorMinar
Copy link
Contributor

Looks reasonable to me. We should make a note about the breaking change (the new behavior is different) and incompatibility with ie8.

@caitp
Copy link
Contributor

caitp commented Jun 27, 2014

We should make a note about the breaking change (the new behavior is different) and incompatibility with ie8.

If we want to backport the fix into 1.2.x, this form of Object.create can be polyfilled, and __proto__ will work in IE8

@IgorMinar
Copy link
Contributor

We are changing the behavior. So we should not backport this.

On Fri, Jun 27, 2014, 6:42 AM, Caitlin Potter notifications@github.com
wrote:

We should make a note about the breaking change (the new behavior is
different) and incompatibility with ie8.

If we want to backport the fix into 1.2.x, this form of Object.create can
be polyfilled, and proto will work in IE8


Reply to this email directly or view it on GitHub
#5063 (comment).

@caitp
Copy link
Contributor

caitp commented Jun 27, 2014

Think of the statement as a note for the next person to come along saying "I want the new features of 1.3, but I need to support IE8, what can I do" =)

@IgorMinar
Copy link
Contributor

Maintain a patch. It's not ideal but you are supporting obsolete browser
already so you have bigger issues.
On Jun 27, 2014 6:45 AM, "Caitlin Potter" notifications@github.com wrote:

Think of the statement as a note for the next person to come along saying
"I want the new features of 1.3, but I need to support IE8, what can I do"
=)


Reply to this email directly or view it on GitHub
#5063 (comment).

@gentooboontoo
Copy link
Contributor Author

I have initially made a branch with missing IE8 polyfills but it made the change ugly and clumsy (especially in branch 1.3 which does not support IE8).
I could try to put them back if asked but a library like es-shims/es5-shim could better help those who want to use missing ES5 features from IE8.

@caitp
Copy link
Contributor

caitp commented Jun 27, 2014

@gentooboontoo that's fine, don't worry about it. We aren't testing IE8 so it's not a big deal to not include such fixes here, since we'll never know if they break or not

@lgalfaso
Copy link
Contributor

@petebacondarwin for 1.3.x, I think this is a no-brainer that we should land this. For 1.2.x I would not back port it

@leider
Copy link

leider commented Jun 27, 2014

what stops you collaborators from pulling?

@caitp
Copy link
Contributor

caitp commented Jun 27, 2014

@leider nothing whatsoever --- I think Pete is merging this one

@gentooboontoo
Copy link
Contributor Author

Thanks a lot @petebacondarwin and @caitp.

@myitcv
Copy link
Contributor

myitcv commented Jun 30, 2014

Thanks from me too.

However this has broken something for me - noted here. Thoughts?

Not sure whether comments on commits get to the right people... hence the 'repost'

@gentooboontoo
Copy link
Contributor Author

I have made another PR (#8034) improving this one preserving own property descriptors and including own non-enumerable properties.

ckknight pushed a commit to ckknight/angular.js that referenced this pull request Jul 16, 2014
So far, angular.copy was copying all properties including those from
prototype chain and was losing the whole prototype chain (except for Date,
Regexp, and Array).

Deep copy should exclude properties from the prototype chain because it
is useless to do so. When modified, properties from prototype chain are
overwritten on the object itself and will be deeply copied then.

Moreover, preserving prototype chain allows instanceof operator to be
consistent between the source object and the copy.
Before this change,

    var Foo = function() {};
    var foo = new Foo();
    var fooCopy = angular.copy(foo);
    foo instanceof Foo; // => true
    fooCopy instanceof Foo; // => false

Now,

    foo instanceof Foo; // => true
    fooCopy instanceof Foo; // => true

The new behaviour is useful when using $http transformResponse. When
receiving JSON data, we could transform it and instantiate real object
"types" from it. The transformed response is always copied by Angular.
The old behaviour was losing the whole prototype chain and broke all
"types" from third-party libraries depending on instanceof.

Closes angular#5063
Closes angular#3767
Closes angular#4996

BREAKING CHANGE:

This changes `angular.copy` so that it applies the prototype of the original
object to the copied object.  Previously, `angular.copy` would copy properties
of the original object's prototype chain directly onto the copied object.

This means that if you iterate over only the copied object's `hasOwnProperty`
properties, it will no longer contain the properties from the prototype.
This is actually much more reasonable behaviour and it is unlikely that
applications are actually relying on this.

If this behaviour is relied upon, in an app, then one should simply iterate
over all the properties on the object (and its inherited properties) and
not filter them with `hasOwnProperty`.

**Be aware that this change also uses a feature that is not compatible with
IE8.**  If you need this to work on IE8 then you would need to provide a polyfill
for `Object.create` and `Object.getPrototypeOf`.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants