jashkenas/underscore — pull request #1911 by maccoyhaha (2014-10-28 10:09:32+00:00)
Title: Resolve the issue there were two types of objects was not iterable

I saw the issues [#148](https://github.com/jashkenas/underscore/issues/148), [#659](https://github.com/jashkenas/underscore/issues/659), [#937](https://github.com/jashkenas/underscore/issues/937), [#1590](https://github.com/jashkenas/underscore/issues/1590),
and I found if use 'obj.length === +obj.length' or 'obj.length !== +obj.length' to judge whether the object is an array or not, there will be two things not iterable.

###### 1. Object which contains 'length' property with 'Number' type value are not iterable:

var obj = { length:3,width:3,height:3 };
_.reduce(obj, function(memo, value){ return memo + value; }); // NaN

###### 2. Objects with [object Function] type are not iterable:

// The value of [object Function]'s 'length' property is arguments' length, and it was unable to be modify.
var func1 = function(){}, func2 = function(a){}, func3 = function(a,b){};
func1.one= func2.one = func3.one = 1, func1.two = func2.two = func3.two = 2;
_.reduce(func1, function(memo, value){ return memo + value; }); // TypeError: Reduce of empty array with no initial value
_.reduce(func2, function(memo, value){ return memo + value; }); // return: undefined
_.reduce(func3, function(memo, value){ return memo + value; }); // return: NaN
_.each(func2, function(value){ console.log(value); }); // log on console: undefined
_.each(func3, function(value){ console.log(value); }); // log on console: undefined undefined

-------------------------------------------

##### Only in the var with String or Array type, 'length' property are 'useful' in only these two cases, so we can use fucntion '_.isObject'.

###### 1. To replace codes like 'obj.length !== +obj.length' in underscore.js with '_.isObject(obj)',
( variable values like {}, function(){}, /reg/, new RegExp, new String, new Date... will all pass,
and var like new String('123') or new Array(1,2) can also be iterated correctly as an object)

###### 2. To replace codes like 'obj.length === +obj.length' with '!_.isObject(obj)',
( only literal value like 'abc' or [1,2,3] will pass ),

##### Then the problem will be resolved:

var obj = { length:3, width:3, height:3 };
_.reduce(obj, function(memo, value){ return memo + value; }); // return: 9

var func = function(a){}; func.one = 1, func.two = 2;
_.reduce(func, function(memo, value){ return memo + value; }, 0); // return: 3
_.each(func, function(value){ console.log(value); }); // log on console: 1 2

var date = new Date();
date.name = 'my date', date.length = 1, date.begin = date.getTime();
_.reduce(date, function( memo, value, key){ return memo + key + '=' + value + '; '; }, 'date info: ');
// return: "date info: name=my date; length=1; begin=1414486668471; "

_.reduce(new String('123'), function(memo, value){ return value + memo; }); // return: "321"
_.reduce('123', function(memo, value){ return value + memo; }); // return: "321"
_.each(new String('123'), function(value, index){ console.log(value, index); }); // log on console: "1" 0 / "2" 1 / "3" 2
_.each('123', function(value, index){ console.log(value, index); }); // log on console: "1" 0 / "2" 1 / "3" 2
_.each(new Array(1,2,3), function(value, index){ console.log(value, index); }); // log on console: 1 "0" / 2 "1" / 3 "2"
_.each([1,2,3], function(value, index){ console.log(value, index); }); // log on console: 1 "0" / 2 "1" / 3 "2"

-------------------------------------------

Functions in underscore.js 1.7.0 which used code like 'obj.length === +obj.length' are:
_.each, _.max, _.min, _.shuffle, _.toArray, _size

Functions which used code like 'obj.length !== +obj.length' are:
_.map, _.reduce, _.reduceRight, _.every, _.some, _.contains, _.

They all pass the test after the modification.
And I reversed some judgement from '!_.isObject(obj)' to '_.isObject(obj)'

-------------------------------------------

#### test case for function 'reduce':

test('reduce', function() {
var sum = _.reduce('123', function(string, char){ return +string + +char; }, 0);
equal(sum, '6', 'can sum up a string with all number type chars');

var rec = _.reduce('123', function(string, char){ return char + string; });
equal(rec, '321', 'can recombine a string');

rec = _.reduce('12ab', function(sum, num){ return sum + ',' + num; });
equal(rec, '1,2,a,b', 'can recombine a string');

sum = _.reduce({length:1,width:2,height:3}, function(sum, num){ return sum + num; }, 0);
equal(sum, 6, 'can sum up an object's all own property value, include its 'lenght' property);

var func = function(a,b){}; // func.length === 2
func.one = 1,func.two = 2,func.three = 3;
sum = _.reduce(func, function(sum, num){ return sum + num; }, 0);
equal(sum, 6, 'can sum up an function's all own property value except its 'lenght' property); // not count the value of 'length' property.

// origin cases all below also pass ---------------------

var sum = _.reduce([1, 2, 3], function(sum, num){ return sum + num; }, 0);
equal(sum, 6, 'can sum up an array');

var context = {multiplier : 3};
sum = _.reduce([1, 2, 3], function(sum, num){ return sum + num * this.multiplier; }, 0, context);
equal(sum, 18, 'can reduce with a context object');

sum = _.inject([1, 2, 3], function(sum, num){ return sum + num; }, 0);
equal(sum, 6, 'aliased as "inject"');

sum = _([1, 2, 3]).reduce(function(sum, num){ return sum + num; }, 0);
equal(sum, 6, 'OO-style reduce');

sum = _.reduce([1, 2, 3], function(sum, num){ return sum + num; });
equal(sum, 6, 'default initial value');

var prod = _.reduce([1, 2, 3, 4], function(prod, num){ return prod * num; });
equal(prod, 24, 'can reduce via multiplication');

ok(_.reduce(null, _.noop, 138) === 138, 'handles a null (with initial value) properly');
equal(_.reduce([], _.noop, undefined), undefined, 'undefined can be passed as a special case');
equal(_.reduce([_], _.noop), _, 'collection of length one with no initial value returns the first item');

throws(function() { _.reduce([], _.noop); }, TypeError, 'throws an error for empty arrays with no initial value');
throws(function() {_.reduce(null, _.noop);}, TypeError, 'handles a null (without initial value) properly');
});