Search code examples
javascriptangularjsreferencedeep-copyshallow-copy

Angular.copy() not deep copying referenced arrays


In my Angular application, I have an array that refers to the coordinates of a polygon. Eg:

[[-1,0], [0,1], [1,0], [0,-1], [-1,0]]

The important bit here is that the that the first and last points are repeated, and actually reference the same 2-length array. This is a result of a plugin I'm using. However, some times the arrays will be created in such a way that the first and last points, while having the same value, are not the same reference.

At a certain point in my Angular application, I need to create a new polygon with the same coordinates as the original, only flipped. My first attempt was this:

var newCoords = angular.copy(polygon.coordinates);
for (var i = 0; i < newCoords.length; i++) {
  newCoords[i].reverse();
}

However, in those instances in which the first and last coordinates have the same reference, I was ending up with one of the points being reversed twice.

My understanding was that angular.copy() creates a deep copy of whatever is passed in, and I shouldn't be experiencing this issue. Clearly this is incorrect, so why? Is there a way to do a truly deep copy of the coordinates array that eliminates that odd reference pairing? I've managed to get around it for now by adding in an additional angular.copy(newCoords[i]) before the reverse().


Solution

  • As suggesting in the comments, this is related to a change inside of angular's core. This change was released in Angular v1.2.17. The bug fix was listed as:

    angular.copy: support circular references in the value being copied (5c997209, #7618)

    However, along with circular references, double references are also handled. To intentionally not handle the double references, you have a few options. Like you mentioned in your post, after preforming a copy, recopy the first index. Not particularly intuitive, but it will work fine in any case:

    var newCoords = angular.copy(polygon.coordinates);
    // Ensure unique reference for the first element
    newCoords[0] = angular.copy(newCoords[0]);
    

    Another option is just to use any Angular versions prior to v1.2.17 (even all the way back to v0.9.0), because their default behavior is to create two different clones for each reference.


    For others that may need to do a deep copy, but do not know the exact location of which duplicate references are (and may even be in subobjects), there is another way that will work with Angular versions prior to v1.4.8. This is to pass a third argument into copy to disable circular reference handling. Note that this will work with all versions prior to v1.2.17 as well, because they will simply ignore the 3rd parameter and do their default behavior:

    var stackSource = angular.extend([], {push:angular.noop});
    var newCoords = angular.copy(polygon.coordinates, null, noopArray);
    

    The third parameter is the undocumented stackSource. By overriding it's push method to do nothing, the circular reference detection is broken. Two important things to note is that cyclical references will error (like v1.2.16 and down), and this doesn't work in v1.4.8 and up due to a performance change. In those cases you would have to write your own deep copy function.