From 5c99720934edc35dd462b1ad02c4d0205683d917 Mon Sep 17 00:00:00 2001 From: rodyhaddad Date: Wed, 28 May 2014 12:14:08 -0700 Subject: [PATCH] fix(angular.copy): support circular references in the value being copied Closes #7618 --- src/Angular.js | 34 +++++++++++++++++++++++++++++----- src/ng/rootScope.js | 2 +- test/AngularSpec.js | 14 ++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index b9658021a..3b632a75b 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -757,7 +757,7 @@ function isLeafNode (node) { */ -function copy(source, destination) { +function copy(source, destination, stackSource, stackDest) { if (isWindow(source) || isScope(source)) { throw ngMinErr('cpws', "Can't copy! Making copies of Window or Scope instances is not supported."); @@ -767,22 +767,40 @@ function copy(source, destination) { destination = source; if (source) { if (isArray(source)) { - destination = copy(source, []); + destination = copy(source, [], stackSource, stackDest); } else if (isDate(source)) { destination = new Date(source.getTime()); } else if (isRegExp(source)) { destination = new RegExp(source.source); } else if (isObject(source)) { - destination = copy(source, {}); + destination = copy(source, {}, stackSource, stackDest); } } } else { if (source === destination) throw ngMinErr('cpi', "Can't copy! Source and destination are identical."); + + stackSource = stackSource || []; + stackDest = stackDest || []; + + if (isObject(source)) { + var index = indexOf(stackSource, source); + if (index !== -1) return stackDest[index]; + + stackSource.push(source); + stackDest.push(destination); + } + + var result; if (isArray(source)) { destination.length = 0; for ( var i = 0; i < source.length; i++) { - destination.push(copy(source[i])); + result = copy(source[i], null, stackSource, stackDest); + if (isObject(source[i])) { + stackSource.push(source[i]); + stackDest.push(result); + } + destination.push(result); } } else { var h = destination.$$hashKey; @@ -790,10 +808,16 @@ function copy(source, destination) { delete destination[key]; }); for ( var key in source) { - destination[key] = copy(source[key]); + result = copy(source[key], null, stackSource, stackDest); + if (isObject(source[key])) { + stackSource.push(source[key]); + stackDest.push(result); + } + destination[key] = result; } setHashKey(destination,h); } + } return destination; } diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 6bcee4053..d31ba3d24 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -643,7 +643,7 @@ function $RootScopeProvider(){ && isNaN(value) && isNaN(last)))) { dirty = true; lastDirtyWatch = watch; - watch.last = watch.eq ? copy(value) : value; + watch.last = watch.eq ? copy(value, null) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 1869611cd..ea7204660 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -146,6 +146,20 @@ describe('angular', function() { // make sure we retain the old key expect(hashKey(dst)).toEqual(h); }); + + it('should handle circular references when circularRefs is turned on', function () { + var a = {b: {a: null}, self: null, selfs: [null, null, [null]]}; + a.b.a = a; + a.self = a; + a.selfs = [a, a.b, [a]]; + + var aCopy = copy(a, null); + expect(aCopy).toEqual(a); + + expect(aCopy).not.toBe(a); + expect(aCopy).toBe(aCopy.self); + expect(aCopy.selfs[2]).not.toBe(a.selfs[2]); + }); }); describe("extend", function() {