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

Commit

Permalink
perf(copy): only validate/clear user specified destination
Browse files Browse the repository at this point in the history
Closes #12068
  • Loading branch information
jbedard authored and petebacondarwin committed Nov 19, 2015
1 parent 0f956b2 commit ada25a9
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 79 deletions.
169 changes: 90 additions & 79 deletions src/Angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -731,100 +731,111 @@ function arrayRemove(array, value) {
</file>
</example>
*/
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.");
}
if (isTypedArray(destination)) {
throw ngMinErr('cpta',
"Can't copy! TypedArray destination cannot be mutated.");
}

if (!destination) {
destination = source;
if (isObject(source)) {
var index;
if (stackSource && (index = stackSource.indexOf(source)) !== -1) {
return stackDest[index];
}
function copy(source, destination) {
var stackSource = [];
var stackDest = [];

// TypedArray, Date and RegExp have specific copy functionality and must be
// pushed onto the stack before returning.
// Array and other objects create the base object and recurse to copy child
// objects. The array/object will be pushed onto the stack when recursed.
if (isArray(source)) {
return copy(source, [], stackSource, stackDest);
} else if (isTypedArray(source)) {
destination = new source.constructor(source);
} else if (isDate(source)) {
destination = new Date(source.getTime());
} else if (isRegExp(source)) {
destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
destination.lastIndex = source.lastIndex;
} else if (isFunction(source.cloneNode)) {
destination = source.cloneNode(true);
} else {
var emptyObject = Object.create(getPrototypeOf(source));
return copy(source, emptyObject, stackSource, stackDest);
}
if (destination) {
if (isTypedArray(destination)) {
throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated.");
}
if (source === destination) {
throw ngMinErr('cpi', "Can't copy! Source and destination are identical.");
}

if (stackDest) {
stackSource.push(source);
stackDest.push(destination);
}
// Empty the destination object
if (isArray(destination)) {
destination.length = 0;
} else {
forEach(destination, function(value, key) {
if (key !== '$$hashKey') {
delete destination[key];
}
});
}
} else {
if (source === destination) throw ngMinErr('cpi',
"Can't copy! Source and destination are identical.");

stackSource = stackSource || [];
stackDest = stackDest || [];
stackSource.push(source);
stackDest.push(destination);
return copyRecurse(source, destination);
}

if (isObject(source)) {
stackSource.push(source);
stackDest.push(destination);
}
return copyElement(source);

function copyRecurse(source, destination) {
var h = destination.$$hashKey;
var result, key;
if (isArray(source)) {
destination.length = 0;
for (var i = 0; i < source.length; i++) {
destination.push(copy(source[i], null, stackSource, stackDest));
for (var i = 0, ii = source.length; i < ii; i++) {
destination.push(copyElement(source[i]));
}
} else {
var h = destination.$$hashKey;
if (isArray(destination)) {
destination.length = 0;
} else {
forEach(destination, function(value, key) {
delete destination[key];
});
} else if (isBlankObject(source)) {
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
for (key in source) {
destination[key] = copyElement(source[key]);
}
if (isBlankObject(source)) {
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
for (key in source) {
destination[key] = copy(source[key], null, stackSource, stackDest);
}
} else if (source && typeof source.hasOwnProperty === 'function') {
// Slow path, which must rely on hasOwnProperty
for (key in source) {
if (source.hasOwnProperty(key)) {
destination[key] = copy(source[key], null, stackSource, stackDest);
}
} else if (source && typeof source.hasOwnProperty === 'function') {
// Slow path, which must rely on hasOwnProperty
for (key in source) {
if (source.hasOwnProperty(key)) {
destination[key] = copyElement(source[key]);
}
} else {
// Slowest path --- hasOwnProperty can't be called as a method
for (key in source) {
if (hasOwnProperty.call(source, key)) {
destination[key] = copy(source[key], null, stackSource, stackDest);
}
}
} else {
// Slowest path --- hasOwnProperty can't be called as a method
for (key in source) {
if (hasOwnProperty.call(source, key)) {
destination[key] = copyElement(source[key]);
}
}
setHashKey(destination,h);
}
setHashKey(destination, h);
return destination;
}

function copyElement(source) {
// Simple values
if (!isObject(source)) {
return source;
}

// Already copied values
var index = stackSource.indexOf(source);
if (index !== -1) {
return stackDest[index];
}

if (isWindow(source) || isScope(source)) {
throw ngMinErr('cpws',
"Can't copy! Making copies of Window or Scope instances is not supported.");
}

var needsRecurse = false;
var destination;

if (isArray(source)) {
destination = [];
needsRecurse = true;
} else if (isTypedArray(source)) {
destination = new source.constructor(source);
} else if (isDate(source)) {
destination = new Date(source.getTime());
} else if (isRegExp(source)) {
destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
destination.lastIndex = source.lastIndex;
} else if (isFunction(source.cloneNode)) {
destination = source.cloneNode(true);
} else {
destination = Object.create(getPrototypeOf(source));
needsRecurse = true;
}

stackSource.push(source);
stackDest.push(destination);

return needsRecurse
? copyRecurse(source, destination)
: destination;
}
return destination;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions test/AngularSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,19 @@ describe('angular', function() {
it('should throw an exception if a Scope is being copied', inject(function($rootScope) {
expect(function() { copy($rootScope.$new()); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
expect(function() { copy({child: $rootScope.$new()}, {}); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
expect(function() { copy([$rootScope.$new()]); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
}));

it('should throw an exception if a Window is being copied', function() {
expect(function() { copy(window); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
expect(function() { copy({child: window}); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
expect(function() { copy([window], []); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
});

it('should throw an exception when source and destination are equivalent', function() {
Expand All @@ -334,6 +342,11 @@ describe('angular', function() {
hashKey(src);
dst = copy(src);
expect(hashKey(dst)).not.toEqual(hashKey(src));

src = {foo: {}};
hashKey(src.foo);
dst = copy(src);
expect(hashKey(src.foo)).not.toEqual(hashKey(dst.foo));
});

it('should retain the previous $$hashKey when copying object with hashKey', function() {
Expand Down

0 comments on commit ada25a9

Please sign in to comment.