-
Notifications
You must be signed in to change notification settings - Fork 27.5k
Copy prototype bug with enumerable properties #8032
Conversation
Thanks for the PR! Please check the items below to help us merge this faster. See the contributing docs for more information.
If you need to make changes to your pull request, you can update the commit with Thanks again for your help! |
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 #5063 Closes #3767 Closes #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`.
@myitcv - Your solution now makes copies of all enumerable properties right up the inheritance chain, which means that copies get instance methods/variables whereas originals were getting these from their prototypes. This is not right. What happens if the prototype changes? Your copies would not receive the changes as they have overridden them. |
@@ -808,14 +808,12 @@ function copy(source, destination, stackSource, stackDest) { | |||
delete destination[key]; | |||
}); | |||
for ( var key in source) { | |||
if(source.hasOwnProperty(key)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should not remove hasOwnProperty
otherwise it will copy properties from prototype chain. I have replaced for...in
and source.hasOwnProperty
with Object.getOwnPropertyNames(source)
to include non-enumerable properties in PR #8034 but I am not sure if this is what you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gentooboontoo - no, we only want to consider enumerable properties. I think we're close to a solution with this
@petebacondarwin - understood, but I think we're also agreed that only copying direct properties is also not right. Just as an aside, we are being bitten here by an 'issue' with CoffeeScript constructors on subclasses (code link): class A
constructor: ->
class B extends A
constructor: ->
a = new A
console.log '(k for k of a):', (k for k, v of a)
b = new B
console.log '(k for k of b):', (k for k, v of b) i.e. constructors are by default non-enumerable, except on sub classes. This issue is relevant here. Back to class A
constructor: ->
Object.defineProperty @prototype, 'test',
get: -> @["_test"]
set: (v) ->
Object.defineProperty @, '_test',
writable: true
enumerable: false
@["_test"] = v
console.log 'We just set test to', v
enumerable: true
a_meth: ->
console.log 'Orig a_meth'
class B extends A
constructor: ->
b_meth: ->
class C extends B
constructor: ->
c_meth: ->
# ------------------
c = new C
c.test = 1
c.blah = 5
workingMakeCopy = (source) ->
destination = Object.create(Object.getPrototypeOf(source))
for key, value of source
if source.hasOwnProperty(key) || !Object.getPrototypeOf(source)[key]?
console.log 'copying', key
destination[key] = value
destination
d = workingMakeCopy c
c.a_meth()
d.a_meth()
A.prototype.a_meth = -> console.log 'New a_meth'
c.a_meth()
d.a_meth() This solution takes advantage of an important feature of properties. From the
A corollary: when an accessor is defined on a prototype, the Looking at the
Therefore I think this gives us what we want, namely:
|
Let's be clear what the problem is here: The current copy functionality applies the correct prototype to the copied object, so the copied object does have access to the What we are not getting is the @myitcv - Your solution is trying to get around this by applying the dynamic class property I think what we need to do is look at how your code is assigning the |
I was going to close this one just as I did with #8034 but then I realized that b59b04f likely introduced a regression for deep dirty-checking. previously we would flatten the property hierarchy of deeply watched objects when creating a copy, but that's not the case any more. so if anyone relied on being able to observe prototype properties then that doesn't work any more. |
@IgorMinar why would that be the case? |
Actually I quite like #8034 - if means that a copy really is a copy with nothing left behind. |
Do you mean like this: At master : http://plnkr.co/edit/NUDQR9ZqYLi8kreoLcrk?p=preview |
Yes, @IgorMinar - I agree this is a regression but I don't think it is really related to @myitcv's problem here. The problem with deep watches is that both the |
Created a new issue for it #8040 |
@petebacondarwin - I agree there are a few issues we are trying to tackle around this change. I also note #8040. As a side note, I'm very happy to defer to both you and @IgorMinar on whether further changes are made to To my mind
The caveat for accessor descriptors (incorrectly defined in my previous message as being identified by class A
constructor: ->
a_meth: -> 42
a_val: 10
Object.defineProperty @prototype, 'test',
get: -> 999
set: (v) ->
b = new A
console.log b.test # 999
b.test = 4
console.log b.test # 999
delete b.test
console.log b.test # 999
console.log b.a_val # 10
b.a_val = 33
console.log b.a_val # 33
delete b.a_val
console.log b.a_val # 10
console.log b.a_meth() # 42
b.a_meth = -> 4
console.log b.a_meth() # 4
b.a_meth = 3
console.log b.a_meth # 3
delete b.a_meth
console.log b.a_meth() # 42 Referring back to your comment, I intentionally made So my final attempt at making this work is as follows. The change is in the use of class A
constructor: ->
Object.defineProperty @prototype, 'test',
get: -> @["_test"]
set: (v) ->
Object.defineProperty @, '_test',
writable: true
enumerable: false
@["_test"] = v
console.log 'We just set test to', v
enumerable: true
a_meth: ->
console.log 'Orig a_meth'
class B extends A
constructor: ->
b_meth: ->
class C extends B
constructor: ->
c_meth: ->
# ------------------
c = new C
c.test = 1 # output: We just set test to 1
c.blah = 5
getPropertyDescriptor = (o, p) ->
# walks from the object up the prototype chain until it
# finds the definition of p
res = null
loop
break if !o? || res?
res = Object.getOwnPropertyDescriptor(o,p)
o = Object.getPrototypeOf(o)
res
isAccessorProperty = (o, p) ->
pd = getPropertyDescriptor o, p
pd? && (pd.get? || pd.set?)
workingMakeCopy = (source) ->
destination = Object.create(Object.getPrototypeOf(source))
for key, value of source
# notice the order is significant here; if we have the property
# identified by key defined as a direct property we definitely
# want to copy it
if source.hasOwnProperty(key) || isAccessorProperty(source, key)
console.log 'copying', key
destination[key] = value
destination
d = workingMakeCopy c # output: We just set test to 1
c.a_meth()
d.a_meth()
A.prototype.a_meth = -> console.log 'New a_meth'
c.a_meth()
d.a_meth() Again I totally respect that if Angular core needs a different version this may not be suitable. |
@myitcv - thanks for the thought and effort you are putting into this subject. I will take a further look at your approach tonight. |
@myitcv - I believe that we can get the same final result more simply by copying all the own properties (enumerable or not) from the source object and leaving the stuff defined on the prototype alone. See this coffeescript This is basically what @gentooboontoo was suggesting in #8034.
|
In this case (using the definition of As one example of something that could go wrong, here is part of the output from your last example:
This confirms a somewhat obvious point (given the implementation) that If, as I do, you rely on side effects of the setter (which I have not included in the test code we are talking about) then this approach does not work. But I think at this point it really does depend what you want your definition of |
Indeed perhaps the 'best' approach would be to allow for the definition of |
@myitcv - I'm sorry to say that we have decided that our version of It looks like you will need to create your own version for this situation. |
@petebacondarwin - no problem at all. As I said a few messages ago, quite happy to defer to you, @IgorMinar and others on this one; even if it does mean a bit of duplication here and there. |
Per this thread
Creating a placeholder for a possible fix which includes associated tests.