From 69ce41807a752015e11f9383b779922c6165a96d Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 14 Mar 2015 13:30:45 -0700 Subject: [PATCH] Add `_.restParam`. --- lodash.src.js | 369 +++++++++++++++++++++++++------------------------- test/test.js | 49 ++++++- 2 files changed, 232 insertions(+), 186 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index 255dd9ccb..6498ba895 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -1792,26 +1792,6 @@ return object; } - /** - * The base implementation of `_.bindAll` without support for individual - * method name arguments. - * - * @private - * @param {Object} object The object to bind and assign the bound methods to. - * @param {string[]} methodNames The object method names to bind. - * @returns {Object} Returns `object`. - */ - function baseBindAll(object, methodNames) { - var index = -1, - length = methodNames.length; - - while (++index < length) { - var key = methodNames[index]; - object[key] = createWrapper(object[key], BIND_FLAG, object); - } - return object; - } - /** * The base implementation of `_.callback` which supports specifying the * number of arguments to provide to `func`. @@ -1937,14 +1917,14 @@ * @private * @param {Function} func The function to delay. * @param {number} wait The number of milliseconds to delay invocation. - * @param {Object} args The `arguments` object to slice and provide to `func`. + * @param {Object} args The arguments provide to `func`. * @returns {number} Returns the timer id. */ - function baseDelay(func, wait, args, fromIndex) { + function baseDelay(func, wait, args) { if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } - return setTimeout(function() { func.apply(undefined, baseSlice(args, fromIndex)); }, wait); + return setTimeout(function() { func.apply(undefined, args); }, wait); } /** @@ -2142,11 +2122,10 @@ * @param {Array} array The array to flatten. * @param {boolean} isDeep Specify a deep flatten. * @param {boolean} isStrict Restrict flattening to arrays and `arguments` objects. - * @param {number} fromIndex The index to start from. * @returns {Array} Returns the new flattened array. */ - function baseFlatten(array, isDeep, isStrict, fromIndex) { - var index = fromIndex - 1, + function baseFlatten(array, isDeep, isStrict) { + var index = -1, length = array.length, resIndex = -1, result = []; @@ -2157,7 +2136,7 @@ if (isObjectLike(value) && isLength(value.length) && (isArray(value) || isArguments(value))) { if (isDeep) { // Recursively flatten arrays (susceptible to call stack limits). - value = baseFlatten(value, isDeep, isStrict, 0); + value = baseFlatten(value, isDeep, isStrict); } var valIndex = -1, valLength = value.length; @@ -2287,30 +2266,6 @@ return result; } - /** - * The base implementation of `_.invoke` which requires additional arguments - * to be provided as an array of arguments rather than individually. - * - * @private - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|string} methodName The name of the method to invoke or - * the function invoked per iteration. - * @param {Array} [args] The arguments to invoke the method with. - * @returns {Array} Returns the array of results. - */ - function baseInvoke(collection, methodName, args) { - var index = -1, - isFunc = typeof methodName == 'function', - length = collection ? collection.length : 0, - result = isLength(length) ? Array(length) : []; - - baseEach(collection, function(value) { - var func = isFunc ? methodName : (value != null && value[methodName]); - result[++index] = func ? func.apply(value, args) : undefined; - }); - return result; - } - /** * The base implementation of `_.isEqual` without support for `this` binding * `customizer` functions. @@ -2647,30 +2602,6 @@ }; } - /** - * The base implementation of `_.pullAt` without support for individual - * index arguments. - * - * @private - * @param {Array} array The array to modify. - * @param {number[]} indexes The indexes of elements to remove. - * @returns {Array} Returns the new array of removed elements. - */ - function basePullAt(array, indexes) { - var length = indexes.length, - result = baseAt(array, indexes); - - indexes.sort(baseCompareAscending); - while (length--) { - var index = parseFloat(indexes[length]); - if (index != previous && isIndex(index)) { - var previous = index; - splice.call(array, index, 1); - } - } - return result; - } - /** * The base implementation of `_.random` without support for argument juggling * and returning floating-point numbers. @@ -2901,7 +2832,7 @@ * @private * @param {*} value The unwrapped value. * @param {Array} actions Actions to peform to resolve the unwrapped value. - * @returns {*} Returns the resolved unwrapped value. + * @returns {*} Returns the resolved value. */ function baseWrapperValue(value, actions) { var result = value; @@ -4360,11 +4291,11 @@ * _.difference([1, 2, 3], [4, 2]); * // => [1, 3] */ - function difference(array) { + var difference = restParam(function(array, values) { return (isArray(array) || isArguments(array)) - ? baseDifference(array, baseFlatten(arguments, false, true, 1)) + ? baseDifference(array, baseFlatten(values, false, true)) : []; - } + }); /** * Creates a slice of `array` with `n` elements dropped from the beginning. @@ -4749,7 +4680,7 @@ if (guard && isIterateeCall(array, isDeep, guard)) { isDeep = false; } - return length ? baseFlatten(array, isDeep, false, 0) : []; + return length ? baseFlatten(array, isDeep) : []; } /** @@ -4767,7 +4698,7 @@ */ function flattenDeep(array) { var length = array ? array.length : 0; - return length ? baseFlatten(array, true, false, 0) : []; + return length ? baseFlatten(array, true) : []; } /** @@ -5039,9 +4970,23 @@ * console.log(evens); * // => [10, 20] */ - function pullAt(array) { - return basePullAt(array || [], baseFlatten(arguments, false, false, 1)); - } + var pullAt = restParam(function(array, indexes) { + array || (array = []); + indexes = baseFlatten(indexes); + + var length = indexes.length, + result = baseAt(array, indexes); + + indexes.sort(baseCompareAscending); + while (length--) { + var index = parseFloat(indexes[length]); + if (index != previous && isIndex(index)) { + var previous = index; + splice.call(array, index, 1); + } + } + return result; + }); /** * Removes all elements from `array` that `predicate` returns truthy for @@ -5434,9 +5379,9 @@ * _.union([1, 2], [4, 2], [2, 1]); * // => [1, 2, 4] */ - function union() { - return baseUniq(baseFlatten(arguments, false, true, 0)); - } + var union = restParam(function(arrays) { + return baseUniq(baseFlatten(arrays, false, true)); + }); /** * Creates a duplicate-value-free version of an array using `SameValueZero` @@ -5558,11 +5503,11 @@ * _.without([1, 2, 1, 3], 1, 2); * // => [3] */ - function without(array) { + var without = restParam(function(array, values) { return (isArray(array) || isArguments(array)) - ? baseDifference(array, baseSlice(arguments, 1)) + ? baseDifference(array, values) : []; - } + }); /** * Creates an array that is the symmetric difference of the provided arrays. @@ -5609,15 +5554,7 @@ * _.zip(['fred', 'barney'], [30, 40], [true, false]); * // => [['fred', 30, true], ['barney', 40, false]] */ - function zip() { - var length = arguments.length, - array = Array(length); - - while (length--) { - array[length] = arguments[length]; - } - return unzip(array); - } + var zip = restParam(unzip); /** * Creates an object composed from arrays of property names and values. Provide @@ -5931,13 +5868,13 @@ * _.at(['fred', 'barney', 'pebbles'], 0, 2); * // => ['fred', 'pebbles'] */ - function at(collection) { + var at = restParam(function(collection, props) { var length = collection ? collection.length : 0; if (isLength(length)) { collection = toIterable(collection); } - return baseAt(collection, baseFlatten(arguments, false, false, 1)); - } + return baseAt(collection, baseFlatten(props)); + }); /** * Creates an object composed of keys generated from the results of running @@ -6449,9 +6386,18 @@ * _.invoke([123, 456], String.prototype.split, ''); * // => [['1', '2', '3'], ['4', '5', '6']] */ - function invoke(collection, methodName) { - return baseInvoke(collection, methodName, baseSlice(arguments, 2)); - } + var invoke = restParam(function(collection, methodName, args) { + var index = -1, + isFunc = typeof methodName == 'function', + length = collection ? collection.length : 0, + result = isLength(length) ? Array(length) : []; + + baseEach(collection, function(value) { + var func = isFunc ? methodName : (value != null && value[methodName]); + result[++index] = func ? func.apply(value, args) : undefined; + }); + return result; + }); /** * Creates an array of values by running each element in `collection` through @@ -6968,17 +6914,24 @@ * _.map(_.sortByAll(users, ['user', 'age']), _.values); * // => [['barney', 26], ['barney', 36], ['fred', 30], ['fred', 40]] */ - function sortByAll(collection) { + function sortByAll() { + var args = arguments, + collection = args[0], + guard = args[3], + index = 0, + length = args.length - 1; + if (collection == null) { return []; } - var args = arguments, - guard = args[3]; - - if (guard && isIterateeCall(args[1], args[2], guard)) { - args = [collection, args[1]]; + var props = Array(length); + while (index < length) { + props[index] = args[++index]; } - return baseSortByOrder(collection, baseFlatten(args, false, false, 1), []); + if (guard && isIterateeCall(args[1], args[2], guard)) { + props = args[1]; + } + return baseSortByOrder(collection, baseFlatten(props), []); } /** @@ -7197,7 +7150,7 @@ * @category Function * @param {Function} func The function to bind. * @param {*} thisArg The `this` binding of `func`. - * @param {...*} [args] The arguments to be partially applied. + * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new bound function. * @example * @@ -7216,16 +7169,14 @@ * bound('hi'); * // => 'hi fred!' */ - function bind(func, thisArg) { + var bind = restParam(function(func, thisArg, partials) { var bitmask = BIND_FLAG; - if (arguments.length > 2) { - var partials = baseSlice(arguments, 2), - holders = replaceHolders(partials, bind.placeholder); - + if (partials.length) { + var holders = replaceHolders(partials, bind.placeholder); bitmask |= PARTIAL_FLAG; } return createWrapper(func, bitmask, thisArg, partials, holders); - } + }); /** * Binds methods of an object to the object itself, overwriting the existing @@ -7255,13 +7206,18 @@ * jQuery('#docs').on('click', view.onClick); * // => logs 'clicked docs' when the element is clicked */ - function bindAll(object) { - return baseBindAll(object, - arguments.length > 1 - ? baseFlatten(arguments, false, false, 1) - : functions(object) - ); - } + var bindAll = restParam(function(object, methodNames) { + methodNames = methodNames.length ? baseFlatten(methodNames) : functions(object); + + var index = -1, + length = methodNames.length; + + while (++index < length) { + var key = methodNames[index]; + object[key] = createWrapper(object[key], BIND_FLAG, object); + } + return object; + }); /** * Creates a function that invokes the method at `object[key]` and prepends @@ -7280,7 +7236,7 @@ * @category Function * @param {Object} object The object the method belongs to. * @param {string} key The key of the method. - * @param {...*} [args] The arguments to be partially applied. + * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new bound function. * @example * @@ -7307,16 +7263,14 @@ * bound('hi'); * // => 'hiya fred!' */ - function bindKey(object, key) { + var bindKey = restParam(function(object, key, partials) { var bitmask = BIND_FLAG | BIND_KEY_FLAG; - if (arguments.length > 2) { - var partials = baseSlice(arguments, 2), - holders = replaceHolders(partials, bindKey.placeholder); - + if (partials.length) { + var holders = replaceHolders(partials, bindKey.placeholder); bitmask |= PARTIAL_FLAG; } return createWrapper(key, bitmask, object, partials, holders); - } + }); /** * Creates a function that accepts one or more arguments of `func` that when @@ -7606,9 +7560,9 @@ * }, 'deferred'); * // logs 'deferred' after one or more milliseconds */ - function defer(func) { - return baseDelay(func, 1, arguments, 1); - } + var defer = restParam(function(func, args) { + return baseDelay(func, 1, args); + }); /** * Invokes `func` after `wait` milliseconds. Any additional arguments are @@ -7628,9 +7582,9 @@ * }, 1000, 'later'); * // => logs 'later' after one second */ - function delay(func, wait) { - return baseDelay(func, wait, arguments, 2); - } + var delay = restParam(function(func, wait, args) { + return baseDelay(func, wait, args); + }); /** * Creates a function that returns the result of invoking the provided @@ -7780,7 +7734,7 @@ /** * Creates a function that is restricted to invoking `func` once. Repeat calls * to the function return the value of the first call. The `func` is invoked - * with the `this` binding of the created function. + * with the `this` binding and arguments of the created function. * * @static * @memberOf _ @@ -7813,7 +7767,7 @@ * @memberOf _ * @category Function * @param {Function} func The function to partially apply arguments to. - * @param {...*} [args] The arguments to be partially applied. + * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new partially applied function. * @example * @@ -7830,12 +7784,10 @@ * greetFred('hi'); * // => 'hi fred' */ - function partial(func) { - var partials = baseSlice(arguments, 1), - holders = replaceHolders(partials, partial.placeholder); - + var partial = restParam(function(func, partials) { + var holders = replaceHolders(partials, partial.placeholder); return createWrapper(func, PARTIAL_FLAG, null, partials, holders); - } + }); /** * This method is like `_.partial` except that partially applied arguments @@ -7851,7 +7803,7 @@ * @memberOf _ * @category Function * @param {Function} func The function to partially apply arguments to. - * @param {...*} [args] The arguments to be partially applied. + * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new partially applied function. * @example * @@ -7868,12 +7820,10 @@ * sayHelloTo('fred'); * // => 'hello fred' */ - function partialRight(func) { - var partials = baseSlice(arguments, 1), - holders = replaceHolders(partials, partialRight.placeholder); - + var partialRight = restParam(function(func, partials) { + var holders = replaceHolders(partials, partialRight.placeholder); return createWrapper(func, PARTIAL_RIGHT_FLAG, null, partials, holders); - } + }); /** * Creates a function that invokes `func` with arguments arranged according @@ -7903,29 +7853,84 @@ * }, [1, 2, 3]); * // => [3, 6, 9] */ - function rearg(func) { - var indexes = baseFlatten(arguments, false, false, 1); - return createWrapper(func, REARG_FLAG, null, null, null, indexes); - } + var rearg = restParam(function(func, indexes) { + return createWrapper(func, REARG_FLAG, null, null, null, baseFlatten(indexes)); + }); /** * Creates a function that invokes `func` with the `this` binding of the - * created function and the array of arguments provided to the created - * function much like [Function#apply](http://es5.github.io/#x15.3.4.3). + * created function and arguments from `start` and beyond provided as an array. + * + * **Note:** This method is based on the ES6 rest parameter. See the + * [MDN Wiki](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters) + * for more details. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @returns {Function} Returns the new function. + * @example + * + * var say = _.restParam(function(what, names) { + * return what + ' ' + _.initial(names).join(', ') + + * (_.size(names) > 1 ? ', & ' : '') + _.last(names); + * }); + * + * say('hello', 'fred', 'barney', 'pebbles'); + * // => 'hello fred, barney, & pebbles' + */ + function restParam(func, start) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + start = nativeMax(typeof start == 'undefined' ? (func.length - 1) : (+start || 0), 0); + return function() { + var args = arguments, + index = -1, + length = nativeMax(args.length - start, 0), + rest = Array(length); + + while (++index < length) { + rest[index] = args[start + index]; + } + switch (start) { + case 0: return func.call(this, rest); + case 1: return func.call(this, args[0], rest); + case 2: return func.call(this, args[0], args[1], rest); + } + var otherArgs = Array(start); + index = -1; + while (++index < start) { + otherArgs[index] = args[index]; + } + otherArgs[start] = rest; + return func.apply(this, otherArgs); + }; + } + + /** + * Creates a function that invokes `func` with the `this` binding of the created + * function and an array of arguments much like [Function#apply](http://es5.github.io/#x15.3.4.3). + * + * **Note:** This method is based on the ES6 spread operator. See the + * [MDN Wiki](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator) + * for more details. * * @static * @memberOf _ * @category Function * @param {Function} func The function to spread arguments over. - * @returns {*} Returns the new function. + * @returns {Function} Returns the new function. * @example * - * var spread = _.spread(function(who, what) { + * var say = _.spread(function(who, what) { * return who + ' says ' + what; * }); * - * spread(['Fred', 'hello']); - * // => 'Fred says hello' + * say(['fred', 'hello']); + * // => 'fred says hello' * * // with a Promise * var numbers = Promise.all([ @@ -8895,14 +8900,14 @@ * _.defaults({ 'user': 'barney' }, { 'age': 36 }, { 'user': 'fred' }); * // => { 'user': 'barney', 'age': 36 } */ - function defaults(object) { + var defaults = restParam(function(args) { + var object = args[0]; if (object == null) { return object; } - var args = arrayCopy(arguments); args.push(assignDefaults); return assign.apply(undefined, args); - } + }); /** * This method is like `_.findIndex` except that it returns the key of the @@ -9476,19 +9481,19 @@ * _.omit(object, _.isNumber); * // => { 'user': 'fred' } */ - function omit(object, predicate, thisArg) { + var omit = restParam(function(object, props) { if (object == null) { return {}; } - if (typeof predicate != 'function') { - var props = arrayMap(baseFlatten(arguments, false, false, 1), String); + if (typeof props[0] != 'function') { + var props = arrayMap(baseFlatten(props), String); return pickByArray(object, baseDifference(keysIn(object), props)); } - predicate = bindCallback(predicate, thisArg, 3); + var predicate = bindCallback(props[0], props[1], 3); return pickByCallback(object, function(value, key, object) { return !predicate(value, key, object); }); - } + }); /** * Creates a two dimensional array of the key-value pairs for `object`, @@ -9543,14 +9548,14 @@ * _.pick(object, _.isString); * // => { 'user': 'fred' } */ - function pick(object, predicate, thisArg) { + var pick = restParam(function(object, props) { if (object == null) { return {}; } - return typeof predicate == 'function' - ? pickByCallback(object, bindCallback(predicate, thisArg, 3)) - : pickByArray(object, baseFlatten(arguments, false, false, 1)); - } + return typeof props[0] == 'function' + ? pickByCallback(object, bindCallback(props[0], props[1], 3)) + : pickByArray(object, baseFlatten(props)); + }); /** * Resolves the value of property `key` on `object`. If the value of `key` is @@ -10695,7 +10700,7 @@ * @static * @memberOf _ * @category Utility - * @param {*} func The function to attempt. + * @param {Function} func The function to attempt. * @returns {*} Returns the `func` result or error object. * @example * @@ -10708,20 +10713,13 @@ * elements = []; * } */ - function attempt() { - var func = arguments[0], - length = arguments.length, - args = Array(length ? (length - 1) : 0); - - while (--length > 0) { - args[length - 1] = arguments[length]; - } + var attempt = restParam(function(func, args) { try { return func.apply(undefined, args); } catch(e) { return isError(e) ? e : new Error(e); } - } + }); /** * Creates a function that invokes `func` with the `this` binding of `thisArg` @@ -11417,6 +11415,7 @@ lodash.reject = reject; lodash.remove = remove; lodash.rest = rest; + lodash.restParam = restParam; lodash.shuffle = shuffle; lodash.slice = slice; lodash.sortBy = sortBy; diff --git a/test/test.js b/test/test.js index a66c4e1c6..2e9ec5273 100644 --- a/test/test.js +++ b/test/test.js @@ -12481,6 +12481,52 @@ /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.restParam'); + + (function() { + function fn(a, b, c) { + return slice.call(arguments); + } + + test('should apply a rest parameter to `func`', 1, function() { + var rp = _.restParam(fn); + deepEqual(rp(1, 2, 3, 4), [1, 2, [3, 4]]); + }); + + test('should work with `start`', 1, function() { + var rp = _.restParam(fn, 1); + deepEqual(rp(1, 2, 3, 4), [1, [2, 3, 4]]); + }); + + test('should treat `start` as `0` for negative or `NaN` values', 1, function() { + var values = [-1, NaN, 'x'], + expected = _.map(values, _.constant([[1, 2, 3, 4]])); + + var actual = _.map(values, function(value) { + var rp = _.restParam(fn, value); + return rp(1, 2, 3, 4); + }); + + deepEqual(actual, expected); + }); + + test('should use an empty array when `start` is not reached', 1, function() { + var rp = _.restParam(fn); + deepEqual(rp(1), [1, undefined, []]); + }); + + test('should not set a `this` binding', 1, function() { + var rp = _.restParam(function(x, y) { + return this[x] + this[y[0]]; + }); + + var object = { 'rp': rp, 'x': 4, 'y': 2 }; + strictEqual(object.rp('x', 'y'), 6); + }); + }()); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.runInContext'); (function() { @@ -16267,6 +16313,7 @@ 'partial', 'partialRight', 'rearg', + 'restParam', 'spread', 'throttle' ]; @@ -16387,7 +16434,7 @@ }); }); - test('should throw an error for falsey arguments', 23, function() { + test('should throw an error for falsey arguments', 24, function() { _.each(rejectFalsey, function(methodName) { var expected = _.map(falsey, _.constant(true)), func = _[methodName];