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

Commit 17eb3d7

Browse files
jbedardpetebacondarwin
authored andcommitted
fix(copy): do not copy the same object twice
1 parent c9ccc80 commit 17eb3d7

File tree

2 files changed

+85
-28
lines changed

2 files changed

+85
-28
lines changed

Diff for: src/Angular.js

+22-26
Original file line numberDiff line numberDiff line change
@@ -743,19 +743,33 @@ function copy(source, destination, stackSource, stackDest) {
743743

744744
if (!destination) {
745745
destination = source;
746-
if (source) {
746+
if (isObject(source)) {
747+
var index;
748+
if (stackSource && (index = stackSource.indexOf(source)) !== -1) {
749+
return stackDest[index];
750+
}
751+
752+
// TypedArray, Date and RegExp have specific copy functionality and must be
753+
// pushed onto the stack before returning.
754+
// Array and other objects create the base object and recurse to copy child
755+
// objects. The array/object will be pushed onto the stack when recursed.
747756
if (isArray(source)) {
748-
destination = copy(source, [], stackSource, stackDest);
757+
return copy(source, [], stackSource, stackDest);
749758
} else if (isTypedArray(source)) {
750759
destination = new source.constructor(source);
751760
} else if (isDate(source)) {
752761
destination = new Date(source.getTime());
753762
} else if (isRegExp(source)) {
754763
destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
755764
destination.lastIndex = source.lastIndex;
756-
} else if (isObject(source)) {
765+
} else {
757766
var emptyObject = Object.create(getPrototypeOf(source));
758-
destination = copy(source, emptyObject, stackSource, stackDest);
767+
return copy(source, emptyObject, stackSource, stackDest);
768+
}
769+
770+
if (stackDest) {
771+
stackSource.push(source);
772+
stackDest.push(destination);
759773
}
760774
}
761775
} else {
@@ -766,9 +780,6 @@ function copy(source, destination, stackSource, stackDest) {
766780
stackDest = stackDest || [];
767781

768782
if (isObject(source)) {
769-
var index = stackSource.indexOf(source);
770-
if (index !== -1) return stackDest[index];
771-
772783
stackSource.push(source);
773784
stackDest.push(destination);
774785
}
@@ -777,12 +788,7 @@ function copy(source, destination, stackSource, stackDest) {
777788
if (isArray(source)) {
778789
destination.length = 0;
779790
for (var i = 0; i < source.length; i++) {
780-
result = copy(source[i], null, stackSource, stackDest);
781-
if (isObject(source[i])) {
782-
stackSource.push(source[i]);
783-
stackDest.push(result);
784-
}
785-
destination.push(result);
791+
destination.push(copy(source[i], null, stackSource, stackDest));
786792
}
787793
} else {
788794
var h = destination.$$hashKey;
@@ -796,37 +802,27 @@ function copy(source, destination, stackSource, stackDest) {
796802
if (isBlankObject(source)) {
797803
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
798804
for (key in source) {
799-
putValue(key, source[key], destination, stackSource, stackDest);
805+
destination[key] = copy(source[key], null, stackSource, stackDest);
800806
}
801807
} else if (source && typeof source.hasOwnProperty === 'function') {
802808
// Slow path, which must rely on hasOwnProperty
803809
for (key in source) {
804810
if (source.hasOwnProperty(key)) {
805-
putValue(key, source[key], destination, stackSource, stackDest);
811+
destination[key] = copy(source[key], null, stackSource, stackDest);
806812
}
807813
}
808814
} else {
809815
// Slowest path --- hasOwnProperty can't be called as a method
810816
for (key in source) {
811817
if (hasOwnProperty.call(source, key)) {
812-
putValue(key, source[key], destination, stackSource, stackDest);
818+
destination[key] = copy(source[key], null, stackSource, stackDest);
813819
}
814820
}
815821
}
816822
setHashKey(destination,h);
817823
}
818824
}
819825
return destination;
820-
821-
function putValue(key, val, destination, stackSource, stackDest) {
822-
// No context allocation, trivial outer scope, easily inlined
823-
var result = copy(val, null, stackSource, stackDest);
824-
if (isObject(val)) {
825-
stackSource.push(val);
826-
stackDest.push(result);
827-
}
828-
destination[key] = result;
829-
}
830826
}
831827

832828
/**

Diff for: test/AngularSpec.js

+63-2
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ describe('angular', function() {
336336
expect(hashKey(dst)).not.toEqual(hashKey(src));
337337
});
338338

339-
it('should retain the previous $$hashKey', function() {
339+
it('should retain the previous $$hashKey when copying object with hashKey', function() {
340340
var src,dst,h;
341341
src = {};
342342
dst = {};
@@ -351,7 +351,21 @@ describe('angular', function() {
351351
expect(hashKey(dst)).toEqual(h);
352352
});
353353

354-
it('should handle circular references when circularRefs is turned on', function() {
354+
it('should retain the previous $$hashKey when copying non-object', function() {
355+
var dst = {};
356+
var h = hashKey(dst);
357+
358+
copy(null, dst);
359+
expect(hashKey(dst)).toEqual(h);
360+
361+
copy(42, dst);
362+
expect(hashKey(dst)).toEqual(h);
363+
364+
copy(new Date(), dst);
365+
expect(hashKey(dst)).toEqual(h);
366+
});
367+
368+
it('should handle circular references', function() {
355369
var a = {b: {a: null}, self: null, selfs: [null, null, [null]]};
356370
a.b.a = a;
357371
a.self = a;
@@ -362,13 +376,60 @@ describe('angular', function() {
362376

363377
expect(aCopy).not.toBe(a);
364378
expect(aCopy).toBe(aCopy.self);
379+
expect(aCopy).toBe(aCopy.selfs[2][0]);
365380
expect(aCopy.selfs[2]).not.toBe(a.selfs[2]);
381+
382+
var copyTo = [];
383+
aCopy = copy(a, copyTo);
384+
expect(aCopy).toBe(copyTo);
385+
expect(aCopy).not.toBe(a);
386+
expect(aCopy).toBe(aCopy.self);
387+
});
388+
389+
it('should handle objects with multiple references', function() {
390+
var b = {};
391+
var a = [b, -1, b];
392+
393+
var aCopy = copy(a);
394+
expect(aCopy[0]).not.toBe(a[0]);
395+
expect(aCopy[0]).toBe(aCopy[2]);
396+
397+
var copyTo = [];
398+
aCopy = copy(a, copyTo);
399+
expect(aCopy).toBe(copyTo);
400+
expect(aCopy[0]).not.toBe(a[0]);
401+
expect(aCopy[0]).toBe(aCopy[2]);
402+
});
403+
404+
it('should handle date/regex objects with multiple references', function() {
405+
var re = /foo/;
406+
var d = new Date();
407+
var o = {re: re, re2: re, d: d, d2: d};
408+
409+
var oCopy = copy(o);
410+
expect(oCopy.re).toBe(oCopy.re2);
411+
expect(oCopy.d).toBe(oCopy.d2);
412+
413+
oCopy = copy(o, {});
414+
expect(oCopy.re).toBe(oCopy.re2);
415+
expect(oCopy.d).toBe(oCopy.d2);
366416
});
367417

368418
it('should clear destination arrays correctly when source is non-array', function() {
369419
expect(copy(null, [1,2,3])).toEqual([]);
370420
expect(copy(undefined, [1,2,3])).toEqual([]);
371421
expect(copy({0: 1, 1: 2}, [1,2,3])).toEqual([1,2]);
422+
expect(copy(new Date(), [1,2,3])).toEqual([]);
423+
expect(copy(/a/, [1,2,3])).toEqual([]);
424+
expect(copy(true, [1,2,3])).toEqual([]);
425+
});
426+
427+
it('should clear destination objects correctly when source is non-array', function() {
428+
expect(copy(null, {0:1,1:2,2:3})).toEqual({});
429+
expect(copy(undefined, {0:1,1:2,2:3})).toEqual({});
430+
expect(copy(new Date(), {0:1,1:2,2:3})).toEqual({});
431+
expect(copy(/a/, {0:1,1:2,2:3})).toEqual({});
432+
expect(copy(true, {0:1,1:2,2:3})).toEqual({});
372433
});
373434

374435
it('should copy objects with no prototype parent', function() {

0 commit comments

Comments
 (0)