Add assignValue and assignMergeValue helpers to make value assignments more consistent across methods.

This commit is contained in:
John-David Dalton
2015-07-26 09:42:21 -07:00
parent ce569e4bc4
commit 731d5b6872
2 changed files with 92 additions and 68 deletions

116
lodash.js
View File

@@ -1517,6 +1517,42 @@
return result; return result;
} }
/**
* Assigns `value` to `key` of `object` if the existing value is not equivalent
* using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
* for equality comparisons.
*
* @private
* @param {Object} object The object to augment.
* @param {string} key The key of the property to assign.
* @param {*} value The value to assign.
*/
function assignValue(object, key, value) {
var oldValue = object[key];
if ((value === value ? (value !== oldValue) : (oldValue === oldValue)) ||
(value === undefined && !(key in object))) {
object[key] = value;
}
}
/**
* This function is like `assignValue` except that it doesn't assign `undefined` values.
*
* @private
* @param {Object} object The object to augment.
* @param {string} key The key of the property to assign.
* @param {*} value The value to assign.
*/
function assignMergeValue(object, key, value) {
var oldValue = object[key];
if ((value !== undefined &&
(value === value ? (value !== oldValue) : (oldValue === oldValue))) ||
(typeof key == 'number' &&
value === undefined && !(key in object))) {
object[key] = value;
}
}
/** /**
* The base implementation of `_.assign` without support for multiple sources * The base implementation of `_.assign` without support for multiple sources
* or `customizer` functions. * or `customizer` functions.
@@ -2235,12 +2271,9 @@
* @param {Function} [customizer] The function to customize merged values. * @param {Function} [customizer] The function to customize merged values.
* @param {Array} [stackA=[]] Tracks traversed source objects. * @param {Array} [stackA=[]] Tracks traversed source objects.
* @param {Array} [stackB=[]] Associates values with source counterparts. * @param {Array} [stackB=[]] Associates values with source counterparts.
* @returns {Object} Returns `object`.
*/ */
function baseMerge(object, source, customizer, stackA, stackB) { function baseMerge(object, source, customizer, stackA, stackB) {
var isSrcArr = isArrayLike(source) && (isArray(source) || isTypedArray(source)), var props = (isArray(source) || isTypedArray(source)) ? undefined : keysIn(source);
props = isSrcArr ? undefined : keysIn(source);
arrayEach(props || source, function(srcValue, key) { arrayEach(props || source, function(srcValue, key) {
if (props) { if (props) {
key = srcValue; key = srcValue;
@@ -2252,20 +2285,13 @@
baseMergeDeep(object, source, key, baseMerge, customizer, stackA, stackB); baseMergeDeep(object, source, key, baseMerge, customizer, stackA, stackB);
} }
else { else {
var value = object[key], var newValue = customizer ? customizer(object[key], srcValue, (key + ''), object, source) : undefined;
result = customizer ? customizer(value, srcValue, key, object, source) : undefined, if (newValue === undefined) {
isCommon = result === undefined; newValue = srcValue;
if (isCommon) {
result = srcValue;
}
if ((result !== undefined || (isSrcArr && !(key in object))) &&
(isCommon || (result === result ? (result !== value) : (value === value)))) {
object[key] = result;
} }
assignMergeValue(object, key, newValue);
} }
}); });
return object;
} }
/** /**
@@ -2281,33 +2307,32 @@
* @param {Function} [customizer] The function to customize assigned values. * @param {Function} [customizer] The function to customize assigned values.
* @param {Array} [stackA=[]] Tracks traversed source objects. * @param {Array} [stackA=[]] Tracks traversed source objects.
* @param {Array} [stackB=[]] Associates values with source counterparts. * @param {Array} [stackB=[]] Associates values with source counterparts.
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
*/ */
function baseMergeDeep(object, source, key, mergeFunc, customizer, stackA, stackB) { function baseMergeDeep(object, source, key, mergeFunc, customizer, stackA, stackB) {
var length = stackA.length, var length = stackA.length,
oldValue = object[key],
srcValue = source[key]; srcValue = source[key];
while (length--) { while (length--) {
if (stackA[length] == srcValue) { if (stackA[length] == srcValue) {
object[key] = stackB[length]; assignMergeValue(object, key, stackB[length]);
return; return;
} }
} }
var value = object[key], var newValue = customizer ? customizer(oldValue, srcValue, (key + ''), object, source) : undefined,
result = customizer ? customizer(value, srcValue, key, object, source) : undefined, isCommon = newValue === undefined;
isCommon = result === undefined;
if (isCommon) { if (isCommon) {
result = srcValue; newValue = srcValue;
if (isArrayLike(srcValue) && (isArray(srcValue) || isTypedArray(srcValue))) { if (isArray(srcValue) || isTypedArray(srcValue)) {
result = isArray(value) newValue = isArray(oldValue)
? value ? oldValue
: (isArrayLike(value) ? copyArray(value) : []); : (isArrayLike(oldValue) ? copyArray(oldValue) : []);
} }
else if (isPlainObject(srcValue) || isArguments(srcValue)) { else if (isPlainObject(srcValue) || isArguments(srcValue)) {
result = isArguments(value) newValue = isArguments(oldValue)
? toPlainObject(value) ? toPlainObject(oldValue)
: (isPlainObject(value) ? value : {}); : (isPlainObject(oldValue) ? oldValue : {});
} }
else { else {
isCommon = false; isCommon = false;
@@ -2316,14 +2341,13 @@
// Add the source value to the stack of traversed objects and associate // Add the source value to the stack of traversed objects and associate
// it with its merged value. // it with its merged value.
stackA.push(srcValue); stackA.push(srcValue);
stackB.push(result); stackB.push(newValue);
if (isCommon) { if (isCommon) {
// Recursively merge objects and arrays (susceptible to call stack limits). // Recursively merge objects and arrays (susceptible to call stack limits).
object[key] = mergeFunc(result, srcValue, customizer, stackA, stackB); mergeFunc(newValue, srcValue, customizer, stackA, stackB);
} else if (result === result ? (result !== value) : (value === value)) {
object[key] = result;
} }
assignMergeValue(object, key, newValue);
} }
/** /**
@@ -2471,9 +2495,10 @@
* *
* @private * @private
* @param {Object} object The object to query. * @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get. * @param {Array|string} path The path of the property to set.
* @param {Function} [customizer] The function to customize cloning. * @param {*} value The value to set.
* @returns {*} Returns the resolved value. * @param {Function} [customizer] The function to customize path creation.
* @returns {Object} Returns `object`.
*/ */
function baseSet(object, path, value, customizer) { function baseSet(object, path, value, customizer) {
path = isKey(path, object) ? [path + ''] : toPath(path); path = isKey(path, object) ? [path + ''] : toPath(path);
@@ -2490,13 +2515,13 @@
nested[key] = value; nested[key] = value;
} }
else { else {
var other = nested[key], var oldValue = nested[key],
result = customizer ? customizer(other, key, nested) : undefined; newValue = customizer ? customizer(oldValue, key, nested) : undefined;
if (result === undefined) { if (newValue === undefined) {
result = other == null ? (isIndex(path[index + 1]) ? [] : {}) : other; newValue = oldValue == null ? (isIndex(path[index + 1]) ? [] : {}) : oldValue;
} }
nested[key] = result; assignValue(nested, key, newValue);
} }
} }
nested = nested[key]; nested = nested[key];
@@ -2925,7 +2950,7 @@
while (++index < length) { while (++index < length) {
var key = props[index]; var key = props[index];
object[key] = source[key]; assignValue(object, key, source[key], object[key]);
} }
return object; return object;
} }
@@ -2949,14 +2974,9 @@
while (++index < length) { while (++index < length) {
var key = props[index], var key = props[index],
value = object[key], newValue = customizer ? customizer(object[key], source[key], key, object, source) : source[key];
result = customizer ? customizer(value, source[key], key, object, source) : source[key];
if (!customizer || assignValue(object, key, newValue);
(result === result ? (result !== value) : (value === value)) ||
(value === undefined && !(key in object))) {
object[key] = result;
}
} }
return object; return object;
} }

View File

@@ -5107,6 +5107,30 @@
}); });
}); });
_.each(['assign', 'assignWith', 'defaults', 'extend', 'extendWith', 'merge', 'mergeWith'], function(methodName) {
var func = _[methodName];
test('`_.' + methodName + '` should not assign values that are the same as their destinations', 4, function() {
_.each(['a', ['a'], { 'a': 1 }, NaN], function(value) {
if (defineProperty) {
var object = {},
pass = true;
defineProperty(object, 'a', {
'get': _.constant(value),
'set': function() { pass = false; }
});
func(object, { 'a': value }, _.identity);
ok(pass, value);
}
else {
skipTest();
}
});
});
});
_.each(['assignWith', 'extendWith', 'mergeWith'], function(methodName) { _.each(['assignWith', 'extendWith', 'mergeWith'], function(methodName) {
var func = _[methodName], var func = _[methodName],
isMergeWith = methodName == 'mergeWith'; isMergeWith = methodName == 'mergeWith';
@@ -5160,26 +5184,6 @@
actual = func({ 'a': 1 }, callback, { 'c': 3 }); actual = func({ 'a': 1 }, callback, { 'c': 3 });
deepEqual(actual, { 'a': 1, 'b': 2, 'c': 3 }); deepEqual(actual, { 'a': 1, 'b': 2, 'c': 3 });
}); });
test('`_.' + methodName + '` should not assign the `customizer` result if it is the same as the destination value', 4, function() {
_.each(['a', ['a'], { 'a': 1 }, NaN], function(value) {
if (defineProperty) {
var object = {},
pass = true;
defineProperty(object, 'a', {
'get': _.constant(value),
'set': function() { pass = false; }
});
func(object, { 'a': value }, _.identity);
ok(pass);
}
else {
skipTest();
}
});
});
}); });
/*--------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/