From ab26945eca3cb25b3797c01310fb6eb0a0752838 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Thu, 9 Jul 2015 19:45:12 -0700 Subject: [PATCH] Add `_.extend` and make it and `_.defaults`, `_.defaultsDeep`, and `_.merge` iterate over inherited properties. --- lodash.src.js | 119 ++++++++++++++++++++------------------------------ test/test.js | 48 ++++++++++---------- 2 files changed, 71 insertions(+), 96 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index c2fa0d802..f21fd8376 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -1646,68 +1646,8 @@ } /** - * Used by `_.defaults` to customize its `_.assign` use. - * - * @private - * @param {*} objectValue The destination object property value. - * @param {*} sourceValue The source object property value. - * @returns {*} Returns the value to assign to the destination object. - */ - function assignDefaults(objectValue, sourceValue) { - return objectValue === undefined ? sourceValue : objectValue; - } - - /** - * Used by `_.template` to customize its `_.assign` use. - * - * **Note:** This function is like `assignDefaults` except that it ignores - * inherited property values when checking if a property is `undefined`. - * - * @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 assignOwnDefaults(objectValue, sourceValue, key, object) { - return (objectValue === undefined || !hasOwnProperty.call(object, key)) - ? sourceValue - : objectValue; - } - - /** - * A specialized version of `_.assign` for customizing assigned values without - * support for argument juggling, multiple sources, and `this` binding `customizer` - * functions. - * - * @private - * @param {Object} object The destination object. - * @param {Object} source The source object. - * @param {Function} customizer The function to customize assigned values. - * @returns {Object} Returns `object`. - */ - function assignWith(object, source, customizer) { - var index = -1, - props = keys(source), - length = props.length; - - while (++index < length) { - var key = props[index], - value = object[key], - result = customizer(value, source[key], key, object, source); - - if ((result === result ? (result !== value) : (value === value)) || - (value === undefined && !(key in object))) { - object[key] = result; - } - } - return object; - } - - /** - * The base implementation of `_.assign` without support for argument juggling, - * multiple sources, and `customizer` functions. + * The base implementation of `_.assign` without support for multiple sources + * and `customizer` functions. * * @private * @param {Object} object The destination object. @@ -1715,9 +1655,7 @@ * @returns {Object} Returns `object`. */ function baseAssign(object, source) { - return source == null - ? object - : baseCopy(source, keys(source), object); + return source == null ? object : copyObject(source, keys(source), object); } /** @@ -3658,6 +3596,18 @@ return true; } + /** + * Used by `_.defaults` to customize its `_.assign` use. + * + * @private + * @param {*} objectValue The destination object property value. + * @param {*} sourceValue The source object property value. + * @returns {*} Returns the value to assign to the destination object. + */ + function extendDefaults(objectValue, sourceValue) { + return objectValue === undefined ? sourceValue : objectValue; + } + /** * Gets metadata for `func`. * @@ -8642,7 +8592,7 @@ return object; } var isSrcArr = isArrayLike(source) && (isArray(source) || isTypedArray(source)), - props = isSrcArr ? undefined : keys(source); + props = isSrcArr ? undefined : keysIn(source); arrayEach(props || source, function(srcValue, key) { if (props) { @@ -8701,9 +8651,10 @@ * // => { 'user': 'barney', 'age': 36 } */ var assign = createAssigner(function(object, source, customizer) { + var props = keys(source); return customizer - ? assignWith(object, source, customizer) - : baseAssign(object, source); + ? copyObjectWith(source, props, customizer, object) + : copyObject(source, props, object) }); /** @@ -8771,8 +8722,8 @@ if (object == null) { return object; } - args.push(assignDefaults); - return assign.apply(undefined, args); + args.push(extendDefaults); + return extend.apply(undefined, args); }); /** @@ -8802,6 +8753,29 @@ return merge.apply(undefined, args); }); + /** + * This method is like `_.assign` except that it iterates over own and + * inherited source properties. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * _.extend({ 'user': 'barney' }, { 'age': 40 }, { 'user': 'fred' }); + * // => { 'user': 'fred', 'age': 40 } + */ + var extend = createAssigner(function(object, source, customizer) { + var props = keysIn(source); + return customizer + ? copyObjectWith(source, props, customizer, object) + : copyObject(source, props, object) + }); + /** * This method is like `_.find` except that it returns the key of the first * element `predicate` returns truthy for instead of the element itself. @@ -10238,9 +10212,9 @@ options = otherOptions = undefined; } string = baseToString(string); - options = assignWith(baseAssign({}, otherOptions || options), settings, assignOwnDefaults); + options = defaults({}, otherOptions || options, settings); - var imports = assignWith(baseAssign({}, options.imports), settings.imports, assignOwnDefaults), + var imports = defaults({}, options.imports, settings.imports), importsKeys = keys(imports), importsValues = baseValues(imports, importsKeys); @@ -11381,6 +11355,7 @@ lodash.dropRight = dropRight; lodash.dropRightWhile = dropRightWhile; lodash.dropWhile = dropWhile; + lodash.extend = extend; lodash.fill = fill; lodash.filter = filter; lodash.flatten = flatten; diff --git a/test/test.js b/test/test.js index 1b196ad3b..943b58338 100644 --- a/test/test.js +++ b/test/test.js @@ -1033,30 +1033,32 @@ /*--------------------------------------------------------------------------*/ - QUnit.module('lodash.assign'); + QUnit.module('lodash.assign and lodash.extend'); - (function() { - test('should assign properties of a source object to the destination object', 1, function() { + _.each(['assign', 'extend'], function(methodName) { + var func = _[methodName]; + + test('`_.' + methodName + '` should assign properties of a source object to the destination object', 1, function() { deepEqual(_.assign({ 'a': 1 }, { 'b': 2 }), { 'a': 1, 'b': 2 }); }); - test('should accept multiple source objects', 2, function() { + test('`_.' + methodName + '` should accept multiple source objects', 2, function() { var expected = { 'a': 1, 'b': 2, 'c': 3 }; deepEqual(_.assign({ 'a': 1 }, { 'b': 2 }, { 'c': 3 }), expected); deepEqual(_.assign({ 'a': 1 }, { 'b': 2, 'c': 2 }, { 'c': 3 }), expected); }); - test('should overwrite destination properties', 1, function() { + test('`_.' + methodName + '` should overwrite destination properties', 1, function() { var expected = { 'a': 3, 'b': 2, 'c': 1 }; deepEqual(_.assign({ 'a': 1, 'b': 2 }, expected), expected); }); - test('should assign source properties with nullish values', 1, function() { + test('`_.' + methodName + '` should assign source properties with nullish values', 1, function() { var expected = { 'a': null, 'b': undefined, 'c': null }; deepEqual(_.assign({ 'a': 1, 'b': 2 }, expected), expected); }); - test('should work with a `customizer` callback', 1, function() { + test('`_.' + methodName + '` should work with a `customizer` callback', 1, function() { var actual = _.assign({ 'a': 1, 'b': 2 }, { 'a': 3, 'c': 3 }, function(a, b) { return typeof a == 'undefined' ? b : a; }); @@ -1064,11 +1066,11 @@ deepEqual(actual, { 'a': 1, 'b': 2, 'c': 3 }); }); - test('should work with a `customizer` that returns `undefined`', 1, function() { + test('`_.' + methodName + '` should work with a `customizer` that returns `undefined`', 1, function() { var expected = { 'a': undefined }; deepEqual(_.assign({}, expected, _.identity), expected); }); - }()); + }); /*--------------------------------------------------------------------------*/ @@ -2829,7 +2831,7 @@ asyncTest('subsequent "immediate" debounced calls return the last `func` result', 2, function() { if (!(isRhino && isModularize)) { - var debounced = _.debounce(_.identity, 32, true), + var debounced = _.debounce(_.identity, 32, { 'leading': true, 'trailing': false }), result = [debounced('x'), debounced('y')]; deepEqual(result, ['x', 'x']); @@ -3787,21 +3789,17 @@ QUnit.module('strict mode checks'); - _.each(['assign', 'bindAll', 'defaults'], function(methodName) { - var func = _[methodName]; + _.each(['assign', 'extend', 'bindAll', 'defaults'], function(methodName) { + var func = _[methodName], + isBindAll = methodName == 'bindAll'; test('`_.' + methodName + '` should ' + (isStrict ? '' : 'not ') + 'throw strict mode errors', 1, function() { if (freeze) { - var object = { 'a': undefined, 'b': function() {} }, + var object = freeze({ 'a': undefined, 'b': function() {} }), pass = !isStrict; - freeze(object); try { - if (methodName == 'bindAll') { - func(object); - } else { - func(object, { 'a': 1 }); - } + func(object, isBindAll ? 'b' : { 'a': 1 }); } catch(e) { pass = !pass; } @@ -5092,8 +5090,9 @@ QUnit.module('object assignments'); - _.each(['assign', 'defaults', 'merge'], function(methodName) { + _.each(['assign', 'defaults', 'extend', 'merge'], function(methodName) { var func = _[methodName], + isAssign = methodName == 'assign', isDefaults = methodName == 'defaults'; test('`_.' + methodName + '` should pass thru falsey `object` values', 1, function() { @@ -5104,13 +5103,14 @@ deepEqual(actual, falsey); }); - test('`_.' + methodName + '` should assign own source properties', 1, function() { + test('`_.' + methodName + '` should assign own ' + (isAssign ? '' : 'and inherited ') + 'source properties', 1, function() { function Foo() { this.a = 1; } Foo.prototype.b = 2; - deepEqual(func({}, new Foo), { 'a': 1 }); + var expected = isAssign ? { 'a': 1 } : { 'a': 1, 'b': 2 }; + deepEqual(func({}, new Foo), expected); }); test('`_.' + methodName + '` should assign problem JScript properties (test in IE < 9)', 1, function() { @@ -5189,7 +5189,7 @@ }); }); - _.each(['assign', 'merge'], function(methodName) { + _.each(['assign', 'extend', 'merge'], function(methodName) { var func = _[methodName], isMerge = methodName == 'merge'; @@ -17476,7 +17476,7 @@ var acceptFalsey = _.difference(allMethods, rejectFalsey); - test('should accept falsey arguments', 211, function() { + test('should accept falsey arguments', 212, function() { var emptyArrays = _.map(falsey, _.constant([])); _.each(acceptFalsey, function(methodName) {