From 74649b5f288048736cba23b6cb2071cfc60a016e Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 4 Jun 2012 14:39:36 -0400 Subject: [PATCH] Add `_.forOwn` and `_.forIn`. Former-commit-id: f4e94a0fd15318063eec16c464435b07f419fa6a --- build.js | 2 + build/pre-compile.js | 2 + lodash.js | 157 +++++++++++++++++++++++++++++-------------- test/test.js | 76 ++++++++++++++++++--- 4 files changed, 177 insertions(+), 60 deletions(-) diff --git a/build.js b/build.js index 31a04ea77..8edd8ef12 100755 --- a/build.js +++ b/build.js @@ -79,6 +79,8 @@ 'first': [], 'flatten': ['isArray'], 'forEach': ['createIterator'], + 'forIn': ['createIterator'], + 'forOwn': ['createIterator'], 'functions': ['createIterator'], 'groupBy': ['createIterator'], 'has': [], diff --git a/build/pre-compile.js b/build/pre-compile.js index 92dbe21f6..2491614c9 100644 --- a/build/pre-compile.js +++ b/build/pre-compile.js @@ -101,6 +101,8 @@ 'foldl', 'foldr', 'forEach', + 'forIn', + 'forOwn', 'functions', 'groupBy', 'has', diff --git a/lodash.js b/lodash.js index ae9b81895..58b37c470 100644 --- a/lodash.js +++ b/lodash.js @@ -323,6 +323,13 @@ 'top': 'if (thisArg) callback = iteratorBind(callback, thisArg)' }; + /** Reusable iterator options for `forIn` and `forOwn` */ + var forOwnIteratorOptions = { + 'inLoop': { + 'object': baseIteratorOptions.inLoop + } + }; + /** Reusable iterator options for `map`, `pluck`, and `values` */ var mapIteratorOptions = { 'init': '', @@ -587,8 +594,9 @@ /** * Checks if the `callback` returns a truthy value for **all** elements of a - * `collection`. The `callback` is invoked with 3 arguments; for arrays they - * are (value, index, array) and for objects they are (value, key, object). + * `collection`. The `callback` is bound to `thisArg` and invoked with 3 + * arguments; for arrays they are (value, index, array) and for objects they + * are (value, key, object). * * @static * @memberOf _ @@ -607,9 +615,9 @@ /** * Examines each value in a `collection`, returning an array of all values the - * `callback` returns truthy for. The `callback` is invoked with 3 arguments; - * for arrays they are (value, index, array) and for objects they are - * (value, key, object). + * `callback` returns truthy for. The `callback` is bound to `thisArg` and + * invoked with 3 arguments; for arrays they are (value, index, array) and for + * objects they are (value, key, object). * * @static * @memberOf _ @@ -630,8 +638,8 @@ * Examines each value in a `collection`, returning the first one the `callback` * returns truthy for. The function returns as soon as it finds an acceptable * value, and does not iterate over the entire `collection`. The `callback` is - * invoked with 3 arguments; for arrays they are (value, index, array) and for - * objects they are (value, key, object). + * bound to `thisArg` and invoked with 3 arguments; for arrays they are + * (value, index, array) and for objects they are (value, key, object). * * @static * @memberOf _ @@ -653,9 +661,9 @@ /** * Iterates over a `collection`, executing the `callback` for each value in the - * `collection`. The `callback` is bound to the `thisArg` value, if one is passed. - * The `callback` is invoked with 3 arguments; for arrays they are - * (value, index, array) and for objects they are (value, key, object). + * `collection`. The `callback` is bound to `thisArg` and invoked with 3 + * arguments; for arrays they are (value, index, array) and for objects they + * are (value, key, object). * * @static * @memberOf _ @@ -667,19 +675,19 @@ * @returns {Array|Object} Returns the `collection`. * @example * - * _.forEach({ 'one': 1, 'two': 2, 'three': 3}, function(num) { alert(num); }); - * // => alerts each number in turn - * * _([1, 2, 3]).forEach(function(num) { alert(num); }).join(','); - * // => alerts each number in turn and returns '1,2,3' + * // => alerts each number and returns '1,2,3' + * + * _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { alert(num); }); + * // => alerts each number (order is not guaranteed) */ var forEach = createIterator(baseIteratorOptions, forEachIteratorOptions); /** * Produces a new array of values by mapping each value in the `collection` - * through a transformation `callback`. The `callback` is bound to the `thisArg` - * value, if one is passed. The `callback` is invoked with 3 arguments; for - * arrays they are (value, index, array) and for objects they are (value, key, object). + * through a transformation `callback`. The `callback` is bound to `thisArg` + * and invoked with 3 arguments; for arrays they are (value, index, array) + * and for objects they are (value, key, object). * * @static * @memberOf _ @@ -695,7 +703,7 @@ * // => [3, 6, 9] * * _.map({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { return num * 3; }); - * // => [3, 6, 9] + * // => [3, 6, 9] (order is not guaranteed) */ var map = createIterator(baseIteratorOptions, mapIteratorOptions); @@ -730,10 +738,9 @@ /** * Boils down a `collection` to a single value. The initial state of the * reduction is `accumulator` and each successive step of it should be returned - * by the `callback`. The `callback` is bound to the `thisArg` value, if one is - * passed. The `callback` is invoked with 4 arguments; for arrays they are - * (accumulator, value, index, array) and for objects they are - * (accumulator, value, key, object). + * by the `callback`. The `callback` is bound to `thisArg` and invoked with 4 + * arguments; for arrays they are (accumulator, value, index, array) and for + * objects they are (accumulator, value, key, object). * * @static * @memberOf _ @@ -769,10 +776,7 @@ }); /** - * The right-associative version of `_.reduce`. The `callback` is bound to the - * `thisArg` value, if one is passed. The `callback` is invoked with 4 arguments; - * for arrays they are (accumulator, value, index, array) and for objects they - * are (accumulator, value, key, object). + * The right-associative version of `_.reduce`. * * @static * @memberOf _ @@ -826,9 +830,7 @@ /** * The opposite of `_.filter`, this method returns the values of a `collection` - * that `callback` does **not** return truthy for. The `callback` is invoked - * with 3 arguments; for arrays they are (value, index, array) and for objects - * they are (value, key, object). + * that `callback` does **not** return truthy for. * * @static * @memberOf _ @@ -849,9 +851,9 @@ /** * Checks if the `callback` returns a truthy value for **any** element of a * `collection`. The function returns as soon as it finds passing value, and - * does not iterate over the entire `collection`. The `callback` is invoked - * with 3 arguments; for arrays they are (value, index, array) and for objects - * they are (value, key, object). + * does not iterate over the entire `collection`. The `callback` is bound to + * `thisArg` and invoked with 3 arguments; for arrays they are + * (value, index, array) and for objects they are (value, key, object). * * @static * @memberOf _ @@ -1040,9 +1042,9 @@ /** * Splits a `collection` into sets, grouped by the result of running each value - * through `callback`. The `callback` is invoked with 3 arguments; - * (value, index, array). The `callback` argument may also be the name of a - * property to group by. + * through `callback`. The `callback` is bound to `thisArg` and invoked with 3 + * arguments; (value, index, array). The `callback` argument may also be the + * name of a property to group by. * * @static * @memberOf _ @@ -1085,9 +1087,8 @@ /** * Produces a new sorted array, ranked in ascending order by the results of * running each value of a `collection` through `callback`. The `callback` is - * invoked with 3 arguments; for arrays they are (value, index, array) and for - * objects they are (value, key, object). The `callback` argument may also be - * the name of a property to sort by (e.g. 'length'). + * bound to `thisArg` and invoked with 3 arguments; (value, index, array). The + * `callback` argument may also be the name of a property to sort by (e.g. 'length'). * * @static * @memberOf _ @@ -1316,8 +1317,8 @@ /** * Retrieves the maximum value of an `array`. If `callback` is passed, * it will be executed for each value in the `array` to generate the - * criterion by which the value is ranked. The `callback` is invoked with 3 - * arguments; (value, index, array). + * criterion by which the value is ranked. The `callback` is bound to + * `thisArg` and invoked with 3 arguments; (value, index, array). * * @static * @memberOf _ @@ -1368,8 +1369,8 @@ /** * Retrieves the minimum value of an `array`. If `callback` is passed, * it will be executed for each value in the `array` to generate the - * criterion by which the value is ranked. The `callback` is invoked with 3 - * arguments; (value, index, array). + * criterion by which the value is ranked. The `callback` is bound to `thisArg` + * and invoked with 3 arguments; (value, index, array). * * @static * @memberOf _ @@ -1513,7 +1514,7 @@ * should be inserted into the `array` in order to maintain the sort order * of the sorted `array`. If `callback` is passed, it will be executed for * `value` and each element in the `array` to compute their sort ranking. - * The `callback` is invoked with 1 argument; (value). + * The `callback` is bound to `thisArg` and invoked with 1 argument; (value). * * @static * @memberOf _ @@ -1596,8 +1597,8 @@ * for comparisons, i.e. `===`. If the `array` is already sorted, passing `true` * for `isSorted` will run a faster algorithm. If `callback` is passed, * each value of `array` is passed through a transformation `callback` before - * uniqueness is computed. The `callback` is invoked with 3 arguments; - * (value, index, array). + * uniqueness is computed. The `callback` is bound to `thisArg` and invoked + * with 3 arguments; (value, index, array). * * @static * @memberOf _ @@ -2222,6 +2223,58 @@ */ var extend = createIterator(extendIteratorOptions); + /** + * Iterates over an `object`'s enumerable own and inherited properties, + * executing the `callback` for each property. The `callback` is bound to + * `thisArg` and invoked with 3 arguments; (value, key, object). + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} callback The function called per iteration. + * @param {Mixed} [thisArg] The `this` binding for the callback. + * @returns {Object} Returns the `object`. + * @example + * + * function Dog(name) { + * this.name = name; + * } + * + * Dog.prototype.bark = function() { + * alert('Woof, woof!'); + * }; + * + * _.forIn(new Dog('Dagny'), function(value, key) { + * alert(key); + * }); + * // => alerts 'name' and 'bark' (order is not guaranteed) + */ + var forIn = createIterator(baseIteratorOptions, forEachIteratorOptions, forOwnIteratorOptions, { + 'useHas': false + }); + + /** + * Iterates over an `object`'s enumerable own properties, executing the + * `callback` for each property. The `callback` is bound to `thisArg` and + * invoked with 3 arguments; (value, key, object). + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} callback The function called per iteration. + * @param {Mixed} [thisArg] The `this` binding for the callback. + * @returns {Object} Returns the `object`. + * @example + * + * _.forOwn({ '0': 'zero', '1': 'one', '2': 'two', 'length': 3 }, function(num, key) { + * alert(key); + * }); + * // => alerts 'zero', 'one', 'two', and 'length' (order is not guaranteed) + */ + var forOwn = createIterator(baseIteratorOptions, forEachIteratorOptions, forOwnIteratorOptions); + /** * Produces a sorted array of the properties, own and inherited, of `object` * that have function values. @@ -2737,7 +2790,7 @@ * @example * * _.keys({ 'one': 1, 'two': 2, 'three': 3 }); - * // => ['one', 'two', 'three'] + * // => ['one', 'two', 'three'] (order is not guaranteed) */ var keys = !nativeKeys ? shimKeys : function(object) { // avoid iterating over the `prototype` property @@ -2780,7 +2833,7 @@ /** * Gets the size of a `value` by returning `value.length` if `value` is a - * string or array, or the number of own enumerable properties if `value` is + * string or array, or the number of enumerable own properties if `value` is * an object. * * @static @@ -2788,7 +2841,7 @@ * @category Objects * @param {Array|Object|String} value The value to inspect. * @returns {Number} Returns `value.length` if `value` is a string or array, - * or the number of own enumerable properties if `value` is an object. + * or the number of enumerable own properties if `value` is an object. * @example * * _.size([1, 2]); @@ -3081,8 +3134,8 @@ } /** - * Executes the `callback` function `n` times. The `callback` is invoked with - * 1 argument; (index). + * Executes the `callback` function `n` times. The `callback` is bound to + * `thisArg` and invoked with 1 argument; (index). * * @static * @memberOf _ @@ -3227,6 +3280,8 @@ lodash.first = first; lodash.flatten = flatten; lodash.forEach = forEach; + lodash.forIn = forIn; + lodash.forOwn = forOwn; lodash.functions = functions; lodash.groupBy = groupBy; lodash.has = has; @@ -3305,7 +3360,7 @@ lodash.take = first; lodash.unique = uniq; - // add pseudo privates used and removed during the build process + // add pseudo private properties used and removed during the build process lodash._createIterator = createIterator; lodash._iteratorTemplate = iteratorTemplate; lodash._shimKeys = shimKeys; diff --git a/test/test.js b/test/test.js index 5aab3a6ab..23b000e18 100644 --- a/test/test.js +++ b/test/test.js @@ -37,6 +37,17 @@ 'valueOf': 7 }; + /** Used to check problem JScript properties too */ + var shadowedKeys = [ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toLocaleString', + 'toString', + 'valueOf' + ]; + /*--------------------------------------------------------------------------*/ /** @@ -222,19 +233,68 @@ var collection = [1, 2, 3]; equal(_.forEach(collection, Boolean), collection); }); + }()); - test('fixes the JScript [[DontEnum]] bug (test in IE < 9)', function() { - var object = {}; - _.forEach(shadowed, function(value, key) { - object[key] = value; - }); + /*--------------------------------------------------------------------------*/ - deepEqual(object, shadowed); + QUnit.module('lodash.forIn'); + + (function() { + test('iterates over inherited properties', function() { + function Dog(name) { this.name = name; } + Dog.prototype.bark = function() { /* Woof, woof! */ }; + + var keys = []; + _.forIn(new Dog('Dagny'), function(value, key) { keys.push(key); }); + deepEqual(keys.sort(), ['bark', 'name']); }); }()); /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.forOwn'); + + (function() { + test('iterates over the `length` property', function() { + var keys = [], + object = { '0': 'zero', '1': 'one', 'length': 2 }; + + _.forOwn(object, function(value, key) { keys.push(key); }); + deepEqual(keys.sort(), ['0', '1', 'length']); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + _.each(['forEach', 'forIn', 'forOwn'], function(methodName) { + var func = _[methodName]; + QUnit.module('lodash.' + methodName + ' iteration bugs'); + + test('fixes the JScript [[DontEnum]] bug (test in IE < 9)', function() { + var keys = []; + func(shadowed, function(value, key) { keys.push(key); }); + deepEqual(keys.sort(), shadowedKeys); + }); + + test('skips the prototype property of functions (test in Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1)', function() { + function Foo() {} + Foo.prototype.a = 1; + + var keys = []; + function callback(value, key) { keys.push(key); } + + func(Foo, callback); + deepEqual(keys, []); + keys.length = 0; + + Foo.prototype = { 'a': 1 }; + func(Foo, callback); + deepEqual(keys, []); + }); + }); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.groupBy'); (function() { @@ -315,7 +375,6 @@ test('skips the prototype property of functions (test in Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1)', function() { function Foo() {} Foo.prototype.a = 1; - equal(_.isEmpty(Foo), true); Foo.prototype = { 'a': 1 }; @@ -353,8 +412,7 @@ Foo.prototype.a = 1; deepEqual(_.keys(Foo.prototype), ['a']); - deepEqual(_.keys(shadowed).sort(), - 'constructor hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf'.split(' ')); + deepEqual(_.keys(shadowed).sort(), shadowedKeys); }); test('skips the prototype property of functions (test in Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1)', function() {