diff --git a/src/Angular.js b/src/Angular.js index 2affd5efb6ac..4685dbde3abd 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -90,6 +90,30 @@ var /** holds major version number for IE or NaN for real browsers */ * @param {Object=} context Object to become context (`this`) for the iterator function. * @returns {Object|Array} Reference to `obj`. */ + + +/** + * @private + * @param {*} obj + * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, ...) + */ +function isArrayLike(obj) { + if (!obj || (typeof obj.length !== 'number')) return false; + + // We have on object which has length property. Should we treat it as array? + if (typeof obj.hasOwnProperty != 'function' && + typeof obj.constructor != 'function') { + // This is here for IE8: it is a bogus object treat it as array; + return true; + } else { + return obj instanceof JQLite || // JQLite + (jQuery && obj instanceof jQuery) || // jQuery + toString.call(obj) !== '[object Object]' || // some browser native object + typeof obj.callee === 'function'; // arguments (on IE8 looks like regular obj) + } +} + + function forEach(obj, iterator, context) { var key; if (obj) { @@ -101,7 +125,7 @@ function forEach(obj, iterator, context) { } } else if (obj.forEach && obj.forEach !== forEach) { obj.forEach(iterator, context); - } else if (isObject(obj) && isNumber(obj.length)) { + } else if (isArrayLike(obj)) { for (key = 0; key < obj.length; key++) iterator.call(context, obj[key], key); } else { diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 09bc902fec73..e29bb16bb58b 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -257,7 +257,7 @@ describe('angular', function() { function MyObj() { this.bar = 'barVal'; this.baz = 'bazVal'; - }; + } MyObj.prototype.foo = 'fooVal'; var obj = new MyObj(), @@ -267,6 +267,77 @@ describe('angular', function() { expect(log).toEqual(['bar:barVal', 'baz:bazVal']); }); + + + it('should handle JQLite and jQuery objects like arrays', function() { + var jqObject = jqLite("

s1s2

").find("span"), + log = []; + + forEach(jqObject, function(value, key) { log.push(key + ':' + value.innerHTML)}); + expect(log).toEqual(['0:s1', '1:s2']); + }); + + + it('should handle NodeList objects like arrays', function() { + var nodeList = jqLite("

abc

")[0].childNodes, + log = []; + + + forEach(nodeList, function(value, key) { log.push(key + ':' + value.innerHTML)}); + expect(log).toEqual(['0:a', '1:b', '2:c']); + }); + + + it('should handle HTMLCollection objects like arrays', function() { + document.body.innerHTML = "

" + + "a" + + "b" + + "c" + + "

"; + + var htmlCollection = document.getElementsByName('x'), + log = []; + + forEach(htmlCollection, function(value, key) { log.push(key + ':' + value.innerHTML)}); + expect(log).toEqual(['0:a', '1:c']); + }); + + + it('should handle arguments objects like arrays', function() { + var args, + log = []; + + (function(){ args = arguments}('a', 'b', 'c')); + + forEach(args, function(value, key) { log.push(key + ':' + value)}); + expect(log).toEqual(['0:a', '1:b', '2:c']); + }); + + + it('should handle objects with length property as objects', function() { + var obj = { + 'foo' : 'bar', + 'length': 2 + }, + log = []; + + forEach(obj, function(value, key) { log.push(key + ':' + value)}); + expect(log).toEqual(['foo:bar', 'length:2']); + }); + + + it('should handle objects of custom types with length property as objects', function() { + function CustomType() { + this.length = 2; + this.foo = 'bar' + } + + var obj = new CustomType(), + log = []; + + forEach(obj, function(value, key) { log.push(key + ':' + value)}); + expect(log).toEqual(['length:2', 'foo:bar']); + }); });