From cdcf18b79c8572ace463d0c5b2f35c63f0508b50 Mon Sep 17 00:00:00 2001 From: Ryan W Tenney Date: Mon, 6 Dec 2010 23:30:43 -0500 Subject: [PATCH] Improve compliance with Array methods in ES5 when passed a null value. (ES5 Array methods treat `null` like `[]`) --- test/arrays.js | 2 ++ test/collections.js | 27 +++++++++++++++++++++++++++ underscore.js | 23 ++++++++++++++++++----- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/test/arrays.js b/test/arrays.js index d3077f41b..c7553fc57 100644 --- a/test/arrays.js +++ b/test/arrays.js @@ -83,6 +83,7 @@ $(document).ready(function() { equals(_.indexOf(numbers, 2), 1, 'can compute indexOf, even without the native function'); var result = (function(){ return _.indexOf(arguments, 2); })(1, 2, 3); equals(result, 1, 'works on an arguments object'); + equals(_.indexOf(null, 2), -1, 'handles nulls properly'); }); test("arrays: lastIndexOf", function() { @@ -92,6 +93,7 @@ $(document).ready(function() { equals(_.lastIndexOf(numbers, 0), 8, 'lastIndexOf the other element'); var result = (function(){ return _.lastIndexOf(arguments, 1); })(1, 0, 1, 0, 0, 1, 0, 0, 0); equals(result, 5, 'works on an arguments object'); + equals(_.indexOf(null, 2), -1, 'handles nulls properly'); }); test("arrays: range", function() { diff --git a/test/collections.js b/test/collections.js index 7cdf16587..442b205f1 100644 --- a/test/collections.js +++ b/test/collections.js @@ -29,6 +29,10 @@ $(document).ready(function() { answers = []; _.each({range : 1, speed : 2, length : 3}, function(v){ answers.push(v); }); ok(answers.join(', '), '1, 2, 3', 'can iterate over objects with numeric length properties'); + + answers = 0; + _.each(null, function(){ ++answers; }); + equals(answers, 0, 'handles a null properly'); }); test('collections: map', function() { @@ -46,6 +50,9 @@ $(document).ready(function() { var ids = _.map(document.images, function(n){ return n.id; }); ok(ids[0] == 'chart_image', 'can use collection methods on HTMLCollections'); + + var ifnull = _.map(null, function(){}); + ok(_.isArray(ifnull) && ifnull.length === 0, 'handles a null properly'); }); test('collections: reduce', function() { @@ -64,6 +71,16 @@ $(document).ready(function() { var sum = _.reduce([1, 2, 3], function(sum, num){ return sum + num; }); equals(sum, 6, 'default initial value'); + + var ifnull; + try { + _.reduce(null, function(){}); + } catch (ex) { + ifnull = ex; + } + ok(ifnull instanceof TypeError, 'handles a null (without inital value) properly'); + + ok(_.reduce(null, function(){}, 138) === 138, 'handles a null (with initial value) properly'); }); test('collections: reduceRight', function() { @@ -75,6 +92,16 @@ $(document).ready(function() { var list = _.foldr(["foo", "bar", "baz"], function(memo, str){ return memo + str; }); equals(list, 'bazbarfoo', 'default initial value'); + + var ifnull; + try { + _.reduceRight(null, function(){}); + } catch (ex) { + ifnull = ex; + } + ok(ifnull instanceof TypeError, 'handles a null (without inital value) properly'); + + ok(_.reduceRight(null, function(){}, 138) === 138, 'handles a null (with initial value) properly'); }); test('collections: detect', function() { diff --git a/underscore.js b/underscore.js index 87c5e4ec8..1dd5b0cf3 100644 --- a/underscore.js +++ b/underscore.js @@ -68,6 +68,7 @@ // Delegates to **ECMAScript 5**'s native `forEach` if available. var each = _.each = _.forEach = function(obj, iterator, context) { var value; + if (obj == null) return; if (nativeForEach && obj.forEach === nativeForEach) { obj.forEach(iterator, context); } else if (_.isNumber(obj.length)) { @@ -86,8 +87,9 @@ // Return the results of applying the iterator to each element. // Delegates to **ECMAScript 5**'s native `map` if available. _.map = function(obj, iterator, context) { - if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); each(obj, function(value, index, list) { results[results.length] = iterator.call(context, value, index, list); }); @@ -98,6 +100,7 @@ // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { var initial = memo !== void 0; + if (obj == null) obj = []; if (nativeReduce && obj.reduce === nativeReduce) { if (context) iterator = _.bind(iterator, context); return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); @@ -105,16 +108,19 @@ each(obj, function(value, index, list) { if (!initial && index === 0) { memo = value; + initial = true; } else { memo = iterator.call(context, memo, value, index, list); } }); + if (!initial) throw new TypeError("Reduce of empty array with no initial value"); return memo; }; // The right-associative version of reduce, also known as `foldr`. // Delegates to **ECMAScript 5**'s native `reduceRight` if available. _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + if (obj == null) obj = []; if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { if (context) iterator = _.bind(iterator, context); return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); @@ -139,8 +145,9 @@ // Delegates to **ECMAScript 5**'s native `filter` if available. // Aliased as `select`. _.filter = _.select = function(obj, iterator, context) { - if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); each(obj, function(value, index, list) { if (iterator.call(context, value, index, list)) results[results.length] = value; }); @@ -150,6 +157,7 @@ // Return all the elements for which a truth test fails. _.reject = function(obj, iterator, context) { var results = []; + if (obj == null) return results; each(obj, function(value, index, list) { if (!iterator.call(context, value, index, list)) results[results.length] = value; }); @@ -161,8 +169,9 @@ // Aliased as `all`. _.every = _.all = function(obj, iterator, context) { iterator = iterator || _.identity; - if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); each(obj, function(value, index, list) { if (!(result = result && iterator.call(context, value, index, list))) return breaker; }); @@ -174,8 +183,9 @@ // Aliased as `any`. var any = _.some = _.any = function(obj, iterator, context) { iterator = iterator || _.identity; - if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); each(obj, function(value, index, list) { if (result = iterator.call(context, value, index, list)) return breaker; }); @@ -185,8 +195,9 @@ // Determine if a given value is included in the array or object using `===`. // Aliased as `contains`. _.include = _.contains = function(obj, target) { - if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; var found = false; + if (obj == null) return found; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; any(obj, function(value) { if (found = value === target) return true; }); @@ -346,6 +357,7 @@ // item in an array, or -1 if the item is not included in the array. // Delegates to **ECMAScript 5**'s native `indexOf` if available. _.indexOf = function(array, item) { + if (array == null) return -1; if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); for (var i = 0, l = array.length; i < l; i++) if (array[i] === item) return i; return -1; @@ -354,6 +366,7 @@ // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. _.lastIndexOf = function(array, item) { + if (array == null) return -1; if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); var i = array.length; while (i--) if (array[i] === item) return i;