From ee4a7034072ff6356b18a20cfc8353f47ca48a71 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sat, 10 May 2014 01:14:52 -0700 Subject: [PATCH] Add `createAssigner`, `defaultsOwn`, and expand the `callback` args of `_.assign` and `_.merge`. --- lodash.js | 87 ++++++++++++++++++++++++++++++++++++++-------------- test/test.js | 77 +++++++++++++++++++++------------------------- 2 files changed, 99 insertions(+), 65 deletions(-) diff --git a/lodash.js b/lodash.js index 2315f57e1..fdacc3b4b 100644 --- a/lodash.js +++ b/lodash.js @@ -248,6 +248,22 @@ return typeof objectValue == 'undefined' ? sourceValue : objectValue; } + /** + * Used by `defaultsOwn` to customize its `_.assign` use. + * + * @private + * @param {*} objectValue The destination object property value. + * @param {*} sourceValue The source object property value. + * @param {string} key The key associated with the object and source values. + * @param {Object} object The destination object. + * @returns {*} Returns the value to assign to the destination object. + */ + function assignDefaultsOwn(objectValue, sourceValue, key, object) { + return (!hasOwnProperty.call(object, key) || typeof objectValue == 'undefined') + ? sourceValue + : objectValue + } + /** * The base implementation of `_.at` without support for strings or individual * key arguments. @@ -1814,17 +1830,17 @@ * @param {Array} [stackB=[]] Associates values with source counterparts. */ function baseMerge(object, source, callback, stackA, stackB) { - (isArray(source) ? arrayEach : baseForOwn)(source, function(source, key) { + (isArray(source) ? arrayEach : baseForOwn)(source, function(srcValue, key, source) { var found, isArr, - result = source, + result = srcValue, value = object[key]; - if (source && ((isArr = isArray(source)) || isPlainObject(source))) { + if (srcValue && ((isArr = isArray(srcValue)) || isPlainObject(srcValue))) { // avoid merging previously merged cyclic sources var stackLength = stackA.length; while (stackLength--) { - if ((found = stackA[stackLength] == source)) { + if ((found = stackA[stackLength] == srcValue)) { value = stackB[stackLength]; break; } @@ -1832,7 +1848,7 @@ if (!found) { var isShallow; if (callback) { - result = callback(value, source); + result = callback(value, srcValue, key, object, source); if ((isShallow = typeof result != 'undefined')) { value = result; } @@ -1843,20 +1859,20 @@ : (isPlainObject(value) ? value : {}); } // add `source` and associated `value` to the stack of traversed objects - stackA.push(source); + stackA.push(srcValue); stackB.push(value); // recursively merge objects and arrays (susceptible to call stack limits) if (!isShallow) { - baseMerge(value, source, callback, stackA, stackB); + baseMerge(value, srcValue, callback, stackA, stackB); } } } else { if (callback) { - result = callback(value, source); + result = callback(value, srcValue, key, object, source); if (typeof result == 'undefined') { - result = source; + result = srcValue; } } if (typeof result != 'undefined') { @@ -2059,6 +2075,27 @@ }; } + /** + * Creates a function that assigns own enumerable properties of source + * object(s) to the destination object executing the callback to produce + * the assigned values. The callback is invoked with five arguments; + * (objectValue, sourceValue, key, object, source). + * + * @private + * @param {Function} [callback] The function to customize assigning values. + * @returns {Function} Returns the new assigner function. + */ + function createAssigner(callback) { + return function(object) { + if (!object) { + return object; + } + var args = slice(arguments); + args.push(callback); + return assign.apply(null, args); + }; + } + /** * Creates a cache object to optimize linear searches of large arrays. * @@ -2203,6 +2240,17 @@ : baseCreateWrapper(data); } + /** + * This method is like `_.defaults` except that it ignores inherited + * property values when checking if a property is `undefined`. + * + * @private + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns the destination object. + */ + var defaultsOwn = createAssigner(assignDefaultsOwn); + /** * Finds the indexes of all placeholder elements in `array`. * @@ -5588,8 +5636,8 @@ * Assigns own enumerable properties of source object(s) to the destination * object. Subsequent sources will overwrite property assignments of previous * sources. If a callback is provided it will be executed to produce the - * assigned values. The callback is bound to `thisArg` and invoked with two - * arguments; (objectValue, sourceValue). + * assigned values. The callback is bound to `thisArg` and invoked with + * five arguments; (objectValue, sourceValue, key, object, source). * * @static * @memberOf _ @@ -5639,7 +5687,7 @@ while (++index < length) { var key = props[index]; - object[key] = callback ? callback(object[key], source[key]) : source[key]; + object[key] = callback ? callback(object[key], source[key], key, object, source) : source[key]; } } return object; @@ -5812,14 +5860,7 @@ * _.defaults({ 'name': 'barney' }, { 'name': 'fred', 'employer': 'slate' }); * // => { 'name': 'barney', 'employer': 'slate' } */ - function defaults(object) { - if (!object) { - return object; - } - var args = slice(arguments); - args.push(assignDefaults); - return assign.apply(null, args); - } + var defaults = createAssigner(assignDefaults); /** * This method is like `_.findIndex` except that it returns the key of the @@ -6781,7 +6822,7 @@ * provided it will be executed to produce the merged values of the destination * and source properties. If the callback returns `undefined` merging will * be handled by the method instead. The callback is bound to `thisArg` and - * invoked with two arguments; (objectValue, sourceValue). + * invoked with five arguments; (objectValue, sourceValue, key, object, source). * * @static * @memberOf _ @@ -7505,10 +7546,10 @@ // and Laura Doktorova's doT.js // https://github.com/olado/doT var settings = lodash.templateSettings; - options = defaults({}, options, settings); + options = defaultsOwn({}, options, settings); string = String(string == null ? '' : string); - var imports = defaults({}, options.imports, settings.imports), + var imports = defaultsOwn({}, options.imports, settings.imports), importsKeys = keys(imports), importsValues = values(imports); diff --git a/test/test.js b/test/test.js index 6e2d2de1a..3171a1bfe 100644 --- a/test/test.js +++ b/test/test.js @@ -639,24 +639,6 @@ deepEqual(actual, { 'a': 1, 'b': 2, 'c': 3 }); }); - test('should pass the correct `callback` arguments', 1, function() { - var args; - - _.assign({ 'a': 1 }, { 'b': 2 }, function() { - args || (args = slice.call(arguments)); - }); - - deepEqual(args, [undefined, 2]); - }); - - test('should support the `thisArg` argument', 1, function() { - var actual = _.assign({ 'a': 1, 'b': 2 }, { 'a': 3, 'c': 3 }, function(a, b) { - return typeof this[a] == 'undefined' ? this[b] : this[a]; - }, { '1': 1, '2': 2, '3': 3 }); - - deepEqual(actual, { 'a': 1, 'b': 2, 'c': 3 }); - }); - test('should be aliased', 1, function() { strictEqual(_.extend, _.assign); }); @@ -3356,26 +3338,46 @@ }); _.each(['assign', 'merge'], function(methodName) { - var func = _[methodName]; + var func = _[methodName], + isMerge = methodName == 'merge'; - test('`_.' + methodName + '` should pass the correct `callback` arguments', 2, function() { - var args; + test('`_.' + methodName + '` should pass the correct `callback` arguments', 3, function() { + var args, + object = { 'a': 1 }, + source = { 'a': 2 }; - func({ 'a': 1 }, { 'a': 2 }, function() { + func(object, source, function() { args || (args = slice.call(arguments)); }); - deepEqual(args, [1, 2], 'primitive property values'); - - var array = [1, 2], - object = { 'b': 2 }; + deepEqual(args, [1, 2, 'a', object, source], 'primitive property values'); args = null; - func({ 'a': array }, { 'a': object }, function() { + object = { 'a': 1 }; + source = { 'b': 2 }; + + func(object, source, function() { args || (args = slice.call(arguments)); }); - deepEqual(args, [array, object], 'non-primitive property values'); + deepEqual(args, [undefined, 2, 'b', object, source], 'missing destination property'); + + var argsList = [], + objectValue = [1, 2], + sourceValue = { 'b': 2 }; + + object = { 'a': objectValue }; + source = { 'a': sourceValue }; + + func(object, source, function() { + argsList.push(slice.call(arguments)); + }); + + var expected = [[objectValue, sourceValue, 'a', object, source]]; + if (isMerge) { + expected.push([undefined, 2, 'b', sourceValue, sourceValue]); + } + deepEqual(argsList, expected, 'non-primitive property values'); }); test('`_.' + methodName + '`should support the `thisArg` argument', 1, function() { @@ -6216,18 +6218,6 @@ }); deepEqual(actual, { 'a': { 'b': [0, 1, 2] } }); }); - - test('should pass the correct values to `callback`', 1, function() { - var argsList = [], - array = [1, 2], - object = { 'b': 2 }; - - _.merge({ 'a': array }, { 'a': object }, function(a, b) { - argsList.push(slice.call(arguments)); - }); - - deepEqual(argsList, [[array, object], [undefined, 2]]); - }); }(1, 2, 3)); /*--------------------------------------------------------------------------*/ @@ -6987,8 +6977,11 @@ source = { 'a': { 'b': 2, 'c': 3 } }, expected = { 'a': { 'b': 1, 'c': 3 } }; - var deepDefaults = _.partialRight(_.merge, _.defaults); - deepEqual(deepDefaults(object, source), expected); + var defaultsDeep = _.partialRight(_.merge, function deep(value, other) { + return _.merge(value, other, deep); + }); + + deepEqual(defaultsDeep(object, source), expected); }); }());