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

Commit f7b9997

Browse files
committed
fix(ngClass): add/remove classes which are properties of Object.prototype
Previously, ngClass and ngAnimate would track the status of classes using an ordinary object. This causes problems when class names match names of properties in Object.prototype, including non-standard Object.prototype properties such as 'watch' and 'unwatch' in Firefox. Because of this shadowing, ngClass and ngAnimate were unable to correctly determine the changed status of these classes. In orderto accomodate this patch, some changes have been necessary elsewhere in the codebase, in order to facilitate iterating, comparingand copying objects with a null prototype, or which shadow the `hasOwnProperty` method Summary: - fast paths for various internal functions when createMap() is used - Make createMap() safe for internal functions like copy/equals/forEach - Use createMap() in more places to avoid needing hasOwnProperty() R=@matsko Closes #11813 Closes #11814
1 parent b2ae35c commit f7b9997

File tree

5 files changed

+172
-15
lines changed

5 files changed

+172
-15
lines changed

src/Angular.js

+57-13
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
isUndefined: true,
3737
isDefined: true,
3838
isObject: true,
39+
isBlankObject: true,
3940
isString: true,
4041
isNumber: true,
4142
isDate: true,
@@ -175,6 +176,7 @@ var
175176
splice = [].splice,
176177
push = [].push,
177178
toString = Object.prototype.toString,
179+
getPrototypeOf = Object.getPrototypeOf,
178180
ngMinErr = minErr('ng'),
179181

180182
/** @name angular */
@@ -267,12 +269,25 @@ function forEach(obj, iterator, context) {
267269
}
268270
} else if (obj.forEach && obj.forEach !== forEach) {
269271
obj.forEach(iterator, context, obj);
270-
} else {
272+
} else if (isBlankObject(obj)) {
273+
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
274+
for (key in obj) {
275+
iterator.call(context, obj[key], key, obj);
276+
}
277+
} else if (typeof obj.hasOwnProperty === 'function') {
278+
// Slow path for objects inheriting Object.prototype, hasOwnProperty check needed
271279
for (key in obj) {
272280
if (obj.hasOwnProperty(key)) {
273281
iterator.call(context, obj[key], key, obj);
274282
}
275283
}
284+
} else {
285+
// Slow path for objects which do not have a method `hasOwnProperty`
286+
for (key in obj) {
287+
if (hasOwnProperty.call(obj, key)) {
288+
iterator.call(context, obj[key], key, obj);
289+
}
290+
}
276291
}
277292
}
278293
return obj;
@@ -498,6 +513,16 @@ function isObject(value) {
498513
}
499514

500515

516+
/**
517+
* Determine if a value is an object with a null prototype
518+
*
519+
* @returns {boolean} True if `value` is an `Object` with a null prototype
520+
*/
521+
function isBlankObject(value) {
522+
return value !== null && typeof value === 'object' && !getPrototypeOf(value);
523+
}
524+
525+
501526
/**
502527
* @ngdoc function
503528
* @name angular.isString
@@ -781,7 +806,7 @@ function copy(source, destination, stackSource, stackDest) {
781806
destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
782807
destination.lastIndex = source.lastIndex;
783808
} else if (isObject(source)) {
784-
var emptyObject = Object.create(Object.getPrototypeOf(source));
809+
var emptyObject = Object.create(getPrototypeOf(source));
785810
destination = copy(source, emptyObject, stackSource, stackDest);
786811
}
787812
}
@@ -800,7 +825,7 @@ function copy(source, destination, stackSource, stackDest) {
800825
stackDest.push(destination);
801826
}
802827

803-
var result;
828+
var result, key;
804829
if (isArray(source)) {
805830
destination.length = 0;
806831
for (var i = 0; i < source.length; i++) {
@@ -820,21 +845,40 @@ function copy(source, destination, stackSource, stackDest) {
820845
delete destination[key];
821846
});
822847
}
823-
for (var key in source) {
824-
if (source.hasOwnProperty(key)) {
825-
result = copy(source[key], null, stackSource, stackDest);
826-
if (isObject(source[key])) {
827-
stackSource.push(source[key]);
828-
stackDest.push(result);
848+
if (isBlankObject(source)) {
849+
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
850+
for (key in source) {
851+
putValue(key, source[key], destination, stackSource, stackDest);
852+
}
853+
} else if (source && typeof source.hasOwnProperty === 'function') {
854+
// Slow path, which must rely on hasOwnProperty
855+
for (key in source) {
856+
if (source.hasOwnProperty(key)) {
857+
putValue(key, source[key], destination, stackSource, stackDest);
858+
}
859+
}
860+
} else {
861+
// Slowest path --- hasOwnProperty can't be called as a method
862+
for (key in source) {
863+
if (hasOwnProperty.call(source, key)) {
864+
putValue(key, source[key], destination, stackSource, stackDest);
829865
}
830-
destination[key] = result;
831866
}
832867
}
833868
setHashKey(destination,h);
834869
}
835-
836870
}
837871
return destination;
872+
873+
function putValue(key, val, destination, stackSource, stackDest) {
874+
// No context allocation, trivial outer scope, easily inlined
875+
var result = copy(val, null, stackSource, stackDest);
876+
if (isObject(val)) {
877+
stackSource.push(val);
878+
stackDest.push(result);
879+
}
880+
destination[key] = result;
881+
}
838882
}
839883

840884
/**
@@ -915,14 +959,14 @@ function equals(o1, o2) {
915959
} else {
916960
if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
917961
isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
918-
keySet = {};
962+
keySet = createMap();
919963
for (key in o1) {
920964
if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
921965
if (!equals(o1[key], o2[key])) return false;
922966
keySet[key] = true;
923967
}
924968
for (key in o2) {
925-
if (!keySet.hasOwnProperty(key) &&
969+
if (!(key in keySet) &&
926970
key.charAt(0) !== '$' &&
927971
o2[key] !== undefined &&
928972
!isFunction(o2[key])) return false;

src/ng/animate.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ function splitClasses(classes) {
2626
classes = classes.split(' ');
2727
}
2828

29-
var obj = {};
29+
// Use createMap() to prevent class assumptions involving property names in
30+
// Object.prototype
31+
var obj = createMap();
3032
forEach(classes, function(klass) {
3133
// sometimes the split leaves empty string values
3234
// incase extra spaces were applied to the options

src/ng/directive/ngClass.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ function classDirective(name, selector) {
3939
}
4040

4141
function digestClassCounts(classes, count) {
42-
var classCounts = element.data('$classCounts') || {};
42+
// Use createMap() to prevent class assumptions involving property
43+
// names in Object.prototype
44+
var classCounts = element.data('$classCounts') || createMap();
4345
var classesToUpdate = [];
4446
forEach(classes, function(className) {
4547
if (count > 0 || classCounts[className]) {

test/AngularSpec.js

+83
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,21 @@ describe('angular', function() {
370370
expect(copy(undefined, [1,2,3])).toEqual([]);
371371
expect(copy({0: 1, 1: 2}, [1,2,3])).toEqual([1,2]);
372372
});
373+
374+
it('should copy objects with no prototype parent', function() {
375+
var obj = extend(Object.create(null), {
376+
a: 1,
377+
b: 2,
378+
c: 3
379+
});
380+
var dest = copy(obj);
381+
382+
expect(Object.getPrototypeOf(dest)).toBe(null);
383+
expect(dest.a).toBe(1);
384+
expect(dest.b).toBe(2);
385+
expect(dest.c).toBe(3);
386+
expect(Object.keys(dest)).toEqual(['a', 'b', 'c']);
387+
});
373388
});
374389

375390
describe("extend", function() {
@@ -651,6 +666,38 @@ describe('angular', function() {
651666
it('should return false when comparing an object and a Date', function() {
652667
expect(equals({}, new Date())).toBe(false);
653668
});
669+
670+
it('should safely compare objects with no prototype parent', function() {
671+
var o1 = extend(Object.create(null), {
672+
a: 1, b: 2, c: 3
673+
});
674+
var o2 = extend(Object.create(null), {
675+
a: 1, b: 2, c: 3
676+
});
677+
expect(equals(o1, o2)).toBe(true);
678+
o2.c = 2;
679+
expect(equals(o1, o2)).toBe(false);
680+
});
681+
682+
683+
it('should safely compare objects which shadow Object.prototype.hasOwnProperty', function() {
684+
/* jshint -W001 */
685+
var o1 = {
686+
hasOwnProperty: true,
687+
a: 1,
688+
b: 2,
689+
c: 3
690+
};
691+
var o2 = {
692+
hasOwnProperty: true,
693+
a: 1,
694+
b: 2,
695+
c: 3
696+
};
697+
expect(equals(o1, o2)).toBe(true);
698+
o1.hasOwnProperty = function() {};
699+
expect(equals(o1, o2)).toBe(false);
700+
});
654701
});
655702

656703

@@ -980,6 +1027,42 @@ describe('angular', function() {
9801027
});
9811028

9821029

1030+
it('should safely iterate through objects with no prototype parent', function() {
1031+
var obj = extend(Object.create(null), {
1032+
a: 1, b: 2, c: 3
1033+
});
1034+
var log = [];
1035+
var self = {};
1036+
forEach(obj, function(val, key, collection) {
1037+
expect(this).toBe(self);
1038+
expect(collection).toBe(obj);
1039+
log.push(key + '=' + val);
1040+
}, self);
1041+
expect(log.length).toBe(3);
1042+
expect(log).toEqual(['a=1', 'b=2', 'c=3']);
1043+
});
1044+
1045+
1046+
it('should safely iterate through objects which shadow Object.prototype.hasOwnProperty', function() {
1047+
/* jshint -W001 */
1048+
var obj = {
1049+
hasOwnProperty: true,
1050+
a: 1,
1051+
b: 2,
1052+
c: 3
1053+
};
1054+
var log = [];
1055+
var self = {};
1056+
forEach(obj, function(val, key, collection) {
1057+
expect(this).toBe(self);
1058+
expect(collection).toBe(obj);
1059+
log.push(key + '=' + val);
1060+
}, self);
1061+
expect(log.length).toBe(4);
1062+
expect(log).toEqual(['hasOwnProperty=true', 'a=1', 'b=2', 'c=3']);
1063+
});
1064+
1065+
9831066
describe('ES spec api compliance', function() {
9841067

9851068
function testForEachSpec(expectedSize, collection) {

test/ng/directive/ngClassSpec.js

+26
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,32 @@ describe('ngClass', function() {
3333
}));
3434

3535

36+
it('should add new and remove old classes with same names as Object.prototype properties dynamically', inject(function($rootScope, $compile) {
37+
/* jshint -W001 */
38+
element = $compile('<div class="existing" ng-class="dynClass"></div>')($rootScope);
39+
$rootScope.dynClass = { watch: true, hasOwnProperty: true, isPrototypeOf: true };
40+
$rootScope.$digest();
41+
expect(element.hasClass('existing')).toBe(true);
42+
expect(element.hasClass('watch')).toBe(true);
43+
expect(element.hasClass('hasOwnProperty')).toBe(true);
44+
expect(element.hasClass('isPrototypeOf')).toBe(true);
45+
46+
$rootScope.dynClass.watch = false;
47+
$rootScope.$digest();
48+
expect(element.hasClass('existing')).toBe(true);
49+
expect(element.hasClass('watch')).toBe(false);
50+
expect(element.hasClass('hasOwnProperty')).toBe(true);
51+
expect(element.hasClass('isPrototypeOf')).toBe(true);
52+
53+
delete $rootScope.dynClass;
54+
$rootScope.$digest();
55+
expect(element.hasClass('existing')).toBe(true);
56+
expect(element.hasClass('watch')).toBe(false);
57+
expect(element.hasClass('hasOwnProperty')).toBe(false);
58+
expect(element.hasClass('isPrototypeOf')).toBe(false);
59+
}));
60+
61+
3662
it('should support adding multiple classes via an array', inject(function($rootScope, $compile) {
3763
element = $compile('<div class="existing" ng-class="[\'A\', \'B\']"></div>')($rootScope);
3864
$rootScope.$digest();

0 commit comments

Comments
 (0)