Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

better handling of Collection types #799

Closed
wants to merge 1 commit into from

4 participants

@afeld

When working on Backbone-Nested I noticed that I wasn't able to iterate over an object with a length property set to an arbitrary value, which is present in the Backbone tests.

_.each({length: 20}, function(){ /* never reached */ });

_.each() and _.size() were handing this case incorrectly, and _.isEmpty() was additionally giving incorrect results for Arguments, HTMLCollections, NodeLists and jQuery Array-likes. I have added _.isArrayLike() so that these methods (which all depend on .length) can detect the types in a central place.

Tests are green in Chrome 22, FF 15 and Safari 6.0. I did some testing in various IE versions as I went but used up my Browserstack limit, so if others wouldn't mind verifying (particularly v6-8), that would be appreciated. Thanks!

@afeld afeld better handling of Collection types
Methods that operate on Collections (_.each(), _.size(), _.isEmpty(),
etc.) are now more consistent for Arguments, HTMLCollections, NodeLists,
and jQuery Array-likes, as well as ensuring that objects with an
arbitrary "length" property are not treated as array-like.  Added an
_.isArrayLike() method for detection.
30f0d32
@afeld

This addresses #448 #252 #659 #741 #148 #708 #724 #690 #784 #496 #653 #708. Phew.

@jdalton
Collaborator

I've handled this using _.forOwn and _.forIn and a more generic check in _.isEmpty without having to resort to extra function calls in collections methods or special casing jQuery/DOM collections.

@jashkenas
Owner

Thanks for the lovely patch, but, as discussed in many previous tickets (as you listed ;) ) our desired semantic check for "array-like" is "numeric length property". We don't want to put all of those special case checks into hot loops, or to miss out on other array-like objects that aren't whitelisted.

@jashkenas jashkenas closed this
@afeld

If that's the case, it should be noted in the docs to be careful of adding a length property to objects. Also, if the final decision is to always use the length, _.isEmpty() isn't consistent with this.

@IonDen
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 27, 2012
  1. @afeld

    better handling of Collection types

    afeld authored
    Methods that operate on Collections (_.each(), _.size(), _.isEmpty(),
    etc.) are now more consistent for Arguments, HTMLCollections, NodeLists,
    and jQuery Array-likes, as well as ensuring that objects with an
    arbitrary "length" property are not treated as array-like.  Added an
    _.isArrayLike() method for detection.
This page is out of date. Refresh to see the latest.
View
13 index.html
@@ -270,6 +270,7 @@
<li>- <a href="#isArray">isArray</a></li>
<li>- <a href="#isObject">isObject</a></li>
<li>- <a href="#isArguments">isArguments</a></li>
+ <li>- <a href="#isArrayLike">isArrayLike</a></li>
<li>- <a href="#isFunction">isFunction</a></li>
<li>- <a href="#isString">isString</a></li>
<li>- <a href="#isNumber">isNumber</a></li>
@@ -1232,6 +1233,18 @@ <h2 id="objects">Object Functions</h2>
=&gt; false
</pre>
+ <p id="isArrayLike">
+ <b class="header">isArrayLike</b><code>_.isArrayLike(object)</code>
+ <br />
+ Returns <i>true</i> if <b>object</b> has a <tt>length</tt> property and can be iterated over with the Collection methods. Currently supported types are Arrays, Arguments, HTMLCollections, NodeLists or jQuery Array-likes.
+ </p>
+ <pre>
+(function(){ return _.isArrayLike(arguments); })(1, 2, 3);
+=&gt; true
+_.isArrayLike({length: 10});
+=&gt; false
+</pre>
+
<p id="isFunction">
<b class="header">isFunction</b><code>_.isFunction(object)</code>
<br />
View
7 test/collections.js
@@ -22,6 +22,12 @@ $(document).ready(function() {
equal(answers.join(", "), 'one, two, three', 'iterating over objects works, and ignores the object prototype.');
delete obj.constructor.prototype.four;
+ answers = [];
+ obj = {one : 1, two : 2, three : 3, length : 12};
+ _.each(obj, function(value, key){ answers.push(key); });
+ equal(answers.join(", "), 'one, two, three, length', 'iterating over objects with a "length" property works.');
+ delete obj.constructor.prototype.four;
+
answer = null;
_.each([1, 2, 3], function(num, index, arr){ if (_.include(arr, num)) answer = true; });
ok(answer, 'can reference the original collection from inside the iterator');
@@ -388,6 +394,7 @@ $(document).ready(function() {
test('size', function() {
equal(_.size({one : 1, two : 2, three : 3}), 3, 'can compute the size of an object');
+ equal(_.size({length : 12}), 1, 'can compute the size of an object with an arbitrary "length" property');
equal(_.size([1, 2, 3]), 3, 'can compute the size of an array');
var func = function() {
View
12 test/objects.js
@@ -428,6 +428,18 @@ $(document).ready(function() {
ok(_.isArray(iArray), 'even from another frame');
});
+ test("isArrayLike", function() {
+ ok(_.isArrayLike(arguments), 'the arguments object is array-like');
+ ok(_.isArrayLike([1, 2, 3]), 'and arrays');
+ ok(_.isArrayLike(iArray), 'even from another frame');
+ ok(_.isArrayLike(document.images), 'and HTMLCollections');
+ if (document.querySelectorAll) {
+ ok(_.isArrayLike(document.querySelectorAll('#map-test *')), 'and NodeLists')
+ }
+ ok(_.isArrayLike($('#map-test').children()), 'and jQuery Array-likes');
+ ok(!_.isArrayLike({length: 10}), 'but not objects with "length" properties');
+ });
+
test("isString", function() {
ok(!_.isString(document.body), 'the document body is not a string');
ok(_.isString([1, 2, 3].join(', ')), 'but strings are');
View
32 underscore.js
@@ -80,7 +80,7 @@
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
- } else if (obj.length === +obj.length) {
+ } else if (_.isArrayLike(obj)) {
for (var i = 0, l = obj.length; i < l; i++) {
if (iterator.call(context, obj[i], i, obj) === breaker) return;
}
@@ -372,7 +372,7 @@
// Return the number of elements in an object.
_.size = function(obj) {
- return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
+ return (_.isString(obj) || _.isArrayLike(obj)) ? obj.length : _.keys(obj).length;
};
// Array Functions
@@ -905,7 +905,7 @@
// An "empty" object has no enumerable own-properties.
_.isEmpty = function(obj) {
if (obj == null) return true;
- if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
+ if (_.isString(obj) || _.isArrayLike(obj)) return obj.length === 0;
for (var key in obj) if (_.has(obj, key)) return false;
return true;
};
@@ -927,11 +927,15 @@
};
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
- each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
- _['is' + name] = function(obj) {
- return toString.call(obj) == '[object ' + name + ']';
- };
- });
+ var isTypes = ['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'];
+ // Use a loop instead of each(), because _.isArrayLike() hasn't been defined.
+ for (var i = 0, len = isTypes.length; i < len; i++){
+ (function(name){
+ _['is' + name] = function(obj) {
+ return toString.call(obj) == '[object ' + name + ']';
+ };
+ }(isTypes[i]));
+ }
// Define a fallback version of the method in browsers (ahem, IE), where
// there isn't any inspectable "Arguments" type.
@@ -948,6 +952,18 @@
};
}
+ // Is a given object an Array, Arguments, HTMLCollection, NodeList or jQuery Array-like?
+ _.isArrayLike = function(obj) {
+ return obj && _.isNumber(obj.length) && (
+ _.isArray(obj) ||
+ _.isArguments(obj) ||
+ /^\[object (HTMLCollection|NodeList)\]$/.test(toString.call(obj)) ||
+ // Fallback for HTMLCollection and NodeList in IE.
+ (typeof obj.item !== 'undefined') ||
+ (typeof jQuery !== 'undefined' && obj instanceof jQuery)
+ );
+ };
+
// Is a given object a finite number?
_.isFinite = function(obj) {
return _.isNumber(obj) && isFinite(obj);
Something went wrong with that request. Please try again.