Add isCloneable and clonePassthru.

This commit is contained in:
John-David Dalton
2014-10-18 00:03:18 -07:00
parent 050b703fc0
commit f2780bcbc2
2 changed files with 83 additions and 34 deletions

View File

@@ -1344,9 +1344,7 @@
* @returns {*} Returns the value to assign to the destination object. * @returns {*} Returns the value to assign to the destination object.
*/ */
function assignDefaults(objectValue, sourceValue) { function assignDefaults(objectValue, sourceValue) {
return typeof objectValue == 'undefined' return typeof objectValue == 'undefined' ? sourceValue : objectValue;
? sourceValue
: objectValue;
} }
/** /**
@@ -1368,6 +1366,18 @@
: objectValue; : objectValue;
} }
/**
* Used by `_.matches` to clone `source` values, letting uncloneable values
* passthu instead of returning empty objects.
*
* @private
* @param {*} value The value to clone.
* @returns {*} Returns the cloned value.
*/
function clonePassthru(value) {
return isCloneable(value) ? undefined : value;
}
/** /**
* The base implementation of `_.assign` without support for argument juggling, * The base implementation of `_.assign` without support for argument juggling,
* multiple sources, and `this` binding. * multiple sources, and `this` binding.
@@ -2965,11 +2975,11 @@
* @returns {null|Object} Returns the initialized object clone. * @returns {null|Object} Returns the initialized object clone.
*/ */
function initObjectClone(object, isDeep) { function initObjectClone(object, isDeep) {
var className = toString.call(object); if (!isCloneable(object)) {
if (!cloneableClasses[className] || isHostObject(object)) {
return null; return null;
} }
var Ctor = object.constructor, var Ctor = object.constructor,
className = toString.call(object),
isArgs = className == argsClass || (!lodash.support.argsClass && isArguments(object)), isArgs = className == argsClass || (!lodash.support.argsClass && isArguments(object)),
isObj = className == objectClass; isObj = className == objectClass;
@@ -3024,6 +3034,17 @@
arrayLikeClasses[toString.call(value)]) || false; arrayLikeClasses[toString.call(value)]) || false;
} }
/**
* Checks if `value` is cloneable.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is cloneable, else `false`.
*/
function isCloneable(value) {
return (value && cloneableClasses[toString.call(value)] && !isHostObject(value)) || false;
}
/** /**
* Checks if `value` is suitable for strict equality comparisons, i.e. `===`. * Checks if `value` is suitable for strict equality comparisons, i.e. `===`.
* *
@@ -6887,8 +6908,8 @@
* **Note:** This method is loosely based on the structured clone algorithm. * **Note:** This method is loosely based on the structured clone algorithm.
* The enumerable properties of `arguments` objects and objects created by * The enumerable properties of `arguments` objects and objects created by
* constructors other than `Object` are cloned to plain `Object` objects. An * constructors other than `Object` are cloned to plain `Object` objects. An
* empty object is returned for functions, DOM nodes, Maps, Sets, and WeakMaps. * empty object is returned for uncloneable values such as functions, DOM nodes,
* See the [HTML5 specification](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm) * Maps, Sets, and WeakMaps. See the [HTML5 specification](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm)
* for more details. * for more details.
* *
* @static * @static
@@ -6944,8 +6965,8 @@
* **Note:** This method is loosely based on the structured clone algorithm. * **Note:** This method is loosely based on the structured clone algorithm.
* The enumerable properties of `arguments` objects and objects created by * The enumerable properties of `arguments` objects and objects created by
* constructors other than `Object` are cloned to plain `Object` objects. An * constructors other than `Object` are cloned to plain `Object` objects. An
* empty object is returned for functions, DOM nodes, Maps, Sets, and WeakMaps. * empty object is returned for uncloneable values such as functions, DOM nodes,
* See the [HTML5 specification](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm) * Maps, Sets, and WeakMaps. See the [HTML5 specification](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm)
* for more details. * for more details.
* *
* @static * @static
@@ -9245,7 +9266,7 @@
value = source[props[index]]; value = source[props[index]];
var isStrict = isStrictComparable(value); var isStrict = isStrictComparable(value);
values[index] = isStrict ? value : baseClone(value, true, matchesCloneCallback); values[index] = isStrict ? value : baseClone(value, true, clonePassthru);
strictCompareFlags[index] = isStrict; strictCompareFlags[index] = isStrict;
} }
return function(object) { return function(object) {

View File

@@ -900,7 +900,7 @@
deepEqual(_.assign({ 'a': 1, 'b': 2 }, expected), expected); deepEqual(_.assign({ 'a': 1, 'b': 2 }, expected), expected);
}); });
test('should work with a callback', 1, function() { test('should work with a `customizer` callback', 1, function() {
var actual = _.assign({ 'a': 1, 'b': 2 }, { 'a': 3, 'c': 3 }, function(a, b) { var actual = _.assign({ 'a': 1, 'b': 2 }, { 'a': 3, 'c': 3 }, function(a, b) {
return typeof a == 'undefined' ? b : a; return typeof a == 'undefined' ? b : a;
}); });
@@ -1625,15 +1625,15 @@
test('`_.cloneDeep` should deep clone objects with circular references', 1, function() { test('`_.cloneDeep` should deep clone objects with circular references', 1, function() {
var object = { var object = {
'foo': { 'b': { 'foo': { 'c': {} } } }, 'foo': { 'b': { 'c': { 'd': {} } } },
'bar': {} 'bar': {}
}; };
object.foo.b.foo.c = object; object.foo.b.c.d = object;
object.bar.b = object.foo.b; object.bar.b = object.foo.b;
var clone = _.cloneDeep(object); var clone = _.cloneDeep(object);
ok(clone.bar.b === clone.foo.b && clone === clone.foo.b.foo.c && clone !== object); ok(clone.bar.b === clone.foo.b && clone === clone.foo.b.c.d && clone !== object);
}); });
_.each(['clone', 'cloneDeep'], function(methodName) { _.each(['clone', 'cloneDeep'], function(methodName) {
@@ -1653,9 +1653,35 @@
}); });
}); });
_.forOwn(nonCloneable, function(object, key) { _.forOwn(nonCloneable, function(value, key) {
test('`_.' + methodName + '` should not clone ' + key, 1, function() { test('`_.' + methodName + '` should not clone ' + key, 2, function() {
deepEqual(func(object), object && {}); var object = { 'a': value, 'b': { 'c': value } },
expected = value && {};
deepEqual(func(value), expected);
expected = isDeep
? { 'a': expected, 'b': { 'c': expected } }
: { 'a': value, 'b': { 'c': value } }
deepEqual(func(object), expected);
});
test('`_.' + methodName + '` should work a `customizer` callback and ' + key, 4, function() {
var customizer = function(value) {
return _.isPlainObject(value) ? undefined : value;
};
var actual = func(value, customizer);
deepEqual(actual, value);
strictEqual(actual, value);
var object = { 'a': value, 'b': { 'c': value } };
actual = func(object, customizer);
deepEqual(actual, object);
notStrictEqual(actual, object);
}); });
}); });
@@ -1699,7 +1725,7 @@
notStrictEqual(actual, shadowedObject); notStrictEqual(actual, shadowedObject);
}); });
test('`_.' + methodName + '` should provide the correct `callback` arguments', 1, function() { test('`_.' + methodName + '` should provide the correct `customizer` arguments', 1, function() {
var argsList = [], var argsList = [],
klass = new Klass; klass = new Klass;
@@ -1718,7 +1744,7 @@
strictEqual(actual, 'A'); strictEqual(actual, 'A');
}); });
test('`_.' + methodName + '` should handle cloning if `callback` returns `undefined`', 1, function() { test('`_.' + methodName + '` should handle cloning if `customizer` returns `undefined`', 1, function() {
var actual = func({ 'a': { 'b': 'c' } }, _.noop); var actual = func({ 'a': { 'b': 'c' } }, _.noop);
deepEqual(actual, { 'a': { 'b': 'c' } }); deepEqual(actual, { 'a': { 'b': 'c' } });
}); });
@@ -4078,10 +4104,12 @@
expected = [1, 2, 3, 4]; expected = [1, 2, 3, 4];
actual = wrapped.flatten(true); actual = wrapped.flatten(true);
ok(actual instanceof _); ok(actual instanceof _);
deepEqual(actual.value(), expected); deepEqual(actual.value(), expected);
actual = wrapped.flattenDeep(); actual = wrapped.flattenDeep();
ok(actual instanceof _); ok(actual instanceof _);
deepEqual(actual.value(), expected); deepEqual(actual.value(), expected);
} }
@@ -4551,7 +4579,7 @@
var func = _[methodName], var func = _[methodName],
isMerge = methodName == 'merge'; isMerge = methodName == 'merge';
test('`_.' + methodName + '` should provide the correct `callback` arguments', 3, function() { test('`_.' + methodName + '` should provide the correct `customizer` arguments', 3, function() {
var args, var args,
object = { 'a': 1 }, object = { 'a': 1 },
source = { 'a': 2 }; source = { 'a': 2 };
@@ -5708,13 +5736,13 @@
strictEqual(_.isEqual(array1, array2), true); strictEqual(_.isEqual(array1, array2), true);
array1.push('a'); array1.push('b');
array2.push('a'); array2.push('b');
strictEqual(_.isEqual(array1, array2), true); strictEqual(_.isEqual(array1, array2), true);
array1.push('b'); array1.push('c');
array2.push('c'); array2.push('d');
strictEqual(_.isEqual(array1, array2), false); strictEqual(_.isEqual(array1, array2), false);
@@ -5773,19 +5801,19 @@
test('should perform comparisons between objects with complex circular references', 1, function() { test('should perform comparisons between objects with complex circular references', 1, function() {
var object1 = { var object1 = {
'foo': { 'b': { 'foo': { 'c': {} } } }, 'foo': { 'b': { 'c': { 'd': {} } } },
'bar': { 'a': 2 } 'bar': { 'a': 2 }
}; };
var object2 = { var object2 = {
'foo': { 'b': { 'foo': { 'c': {} } } }, 'foo': { 'b': { 'c': { 'd': {} } } },
'bar': { 'a': 2 } 'bar': { 'a': 2 }
}; };
object1.foo.b.foo.c = object1; object1.foo.b.c.d = object1;
object1.bar.b = object1.foo.b; object1.bar.b = object1.foo.b;
object2.foo.b.foo.c = object2; object2.foo.b.c.d = object2;
object2.bar.b = object2.foo.b; object2.bar.b = object2.foo.b;
strictEqual(_.isEqual(object1, object2), true); strictEqual(_.isEqual(object1, object2), true);
@@ -5940,7 +5968,7 @@
deepEqual(actual, expected); deepEqual(actual, expected);
}); });
test('should provide the correct `callback` arguments', 1, function() { test('should provide the correct `customizer` arguments', 1, function() {
var argsList = []; var argsList = [];
var object1 = { var object1 = {
@@ -7682,15 +7710,15 @@
}; };
var source = { var source = {
'foo': { 'b': { 'foo': { 'c': {} } } }, 'foo': { 'b': { 'c': { 'd': {} } } },
'bar': {} 'bar': {}
}; };
source.foo.b.foo.c = source; source.foo.b.c.d = source;
source.bar.b = source.foo.b; source.bar.b = source.foo.b;
var actual = _.merge(object, source); var actual = _.merge(object, source);
ok(actual.bar.b === actual.foo.b && actual.foo.b.foo.c === actual.foo.b.foo.c.foo.b.foo.c); ok(actual.bar.b === actual.foo.b && actual.foo.b.c.d === actual.foo.b.c.d.foo.b.c.d);
}); });
test('should treat sources that are sparse arrays as dense', 2, function() { test('should treat sources that are sparse arrays as dense', 2, function() {
@@ -7735,12 +7763,12 @@
deepEqual(actual, { 'a': 1 }); deepEqual(actual, { 'a': 1 });
}); });
test('should handle merging if `callback` returns `undefined`', 1, function() { test('should handle merging if `customizer` returns `undefined`', 1, function() {
var actual = _.merge({ 'a': { 'b': [1, 1] } }, { 'a': { 'b': [0] } }, _.noop); var actual = _.merge({ 'a': { 'b': [1, 1] } }, { 'a': { 'b': [0] } }, _.noop);
deepEqual(actual, { 'a': { 'b': [0, 1] } }); deepEqual(actual, { 'a': { 'b': [0, 1] } });
}); });
test('should defer to `callback` when it returns a value other than `undefined`', 1, function() { test('should defer to `customizer` when it returns a value other than `undefined`', 1, function() {
var actual = _.merge({ 'a': { 'b': [0, 1] } }, { 'a': { 'b': [2] } }, function(a, b) { var actual = _.merge({ 'a': { 'b': [0, 1] } }, { 'a': { 'b': [2] } }, function(a, b) {
return _.isArray(a) ? a.concat(b) : undefined; return _.isArray(a) ? a.concat(b) : undefined;
}); });