diff --git a/lodash.js b/lodash.js index c50b9e53d..1e7167749 100644 --- a/lodash.js +++ b/lodash.js @@ -1344,9 +1344,7 @@ * @returns {*} Returns the value to assign to the destination object. */ function assignDefaults(objectValue, sourceValue) { - return typeof objectValue == 'undefined' - ? sourceValue - : objectValue; + return typeof objectValue == 'undefined' ? sourceValue : objectValue; } /** @@ -1368,6 +1366,18 @@ : 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, * multiple sources, and `this` binding. @@ -2965,11 +2975,11 @@ * @returns {null|Object} Returns the initialized object clone. */ function initObjectClone(object, isDeep) { - var className = toString.call(object); - if (!cloneableClasses[className] || isHostObject(object)) { + if (!isCloneable(object)) { return null; } var Ctor = object.constructor, + className = toString.call(object), isArgs = className == argsClass || (!lodash.support.argsClass && isArguments(object)), isObj = className == objectClass; @@ -3024,6 +3034,17 @@ 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. `===`. * @@ -6887,8 +6908,8 @@ * **Note:** This method is loosely based on the structured clone algorithm. * The enumerable properties of `arguments` objects and objects created by * constructors other than `Object` are cloned to plain `Object` objects. An - * empty object is returned for functions, DOM nodes, Maps, Sets, and WeakMaps. - * See the [HTML5 specification](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm) + * empty object is returned for uncloneable values such as functions, DOM nodes, + * Maps, Sets, and WeakMaps. See the [HTML5 specification](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm) * for more details. * * @static @@ -6944,8 +6965,8 @@ * **Note:** This method is loosely based on the structured clone algorithm. * The enumerable properties of `arguments` objects and objects created by * constructors other than `Object` are cloned to plain `Object` objects. An - * empty object is returned for functions, DOM nodes, Maps, Sets, and WeakMaps. - * See the [HTML5 specification](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm) + * empty object is returned for uncloneable values such as functions, DOM nodes, + * Maps, Sets, and WeakMaps. See the [HTML5 specification](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm) * for more details. * * @static @@ -9245,7 +9266,7 @@ value = source[props[index]]; var isStrict = isStrictComparable(value); - values[index] = isStrict ? value : baseClone(value, true, matchesCloneCallback); + values[index] = isStrict ? value : baseClone(value, true, clonePassthru); strictCompareFlags[index] = isStrict; } return function(object) { diff --git a/test/test.js b/test/test.js index 8ead32984..e654f1435 100644 --- a/test/test.js +++ b/test/test.js @@ -900,7 +900,7 @@ 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) { return typeof a == 'undefined' ? b : a; }); @@ -1625,15 +1625,15 @@ test('`_.cloneDeep` should deep clone objects with circular references', 1, function() { var object = { - 'foo': { 'b': { 'foo': { 'c': {} } } }, + 'foo': { 'b': { 'c': { 'd': {} } } }, 'bar': {} }; - object.foo.b.foo.c = object; + object.foo.b.c.d = object; object.bar.b = object.foo.b; 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) { @@ -1653,9 +1653,35 @@ }); }); - _.forOwn(nonCloneable, function(object, key) { - test('`_.' + methodName + '` should not clone ' + key, 1, function() { - deepEqual(func(object), object && {}); + _.forOwn(nonCloneable, function(value, key) { + test('`_.' + methodName + '` should not clone ' + key, 2, function() { + 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); }); - test('`_.' + methodName + '` should provide the correct `callback` arguments', 1, function() { + test('`_.' + methodName + '` should provide the correct `customizer` arguments', 1, function() { var argsList = [], klass = new Klass; @@ -1718,7 +1744,7 @@ 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); deepEqual(actual, { 'a': { 'b': 'c' } }); }); @@ -4078,10 +4104,12 @@ expected = [1, 2, 3, 4]; actual = wrapped.flatten(true); + ok(actual instanceof _); deepEqual(actual.value(), expected); actual = wrapped.flattenDeep(); + ok(actual instanceof _); deepEqual(actual.value(), expected); } @@ -4551,7 +4579,7 @@ var func = _[methodName], 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, object = { 'a': 1 }, source = { 'a': 2 }; @@ -5708,13 +5736,13 @@ strictEqual(_.isEqual(array1, array2), true); - array1.push('a'); - array2.push('a'); + array1.push('b'); + array2.push('b'); strictEqual(_.isEqual(array1, array2), true); - array1.push('b'); - array2.push('c'); + array1.push('c'); + array2.push('d'); strictEqual(_.isEqual(array1, array2), false); @@ -5773,19 +5801,19 @@ test('should perform comparisons between objects with complex circular references', 1, function() { var object1 = { - 'foo': { 'b': { 'foo': { 'c': {} } } }, + 'foo': { 'b': { 'c': { 'd': {} } } }, 'bar': { 'a': 2 } }; var object2 = { - 'foo': { 'b': { 'foo': { 'c': {} } } }, + 'foo': { 'b': { 'c': { 'd': {} } } }, 'bar': { 'a': 2 } }; - object1.foo.b.foo.c = object1; + object1.foo.b.c.d = object1; object1.bar.b = object1.foo.b; - object2.foo.b.foo.c = object2; + object2.foo.b.c.d = object2; object2.bar.b = object2.foo.b; strictEqual(_.isEqual(object1, object2), true); @@ -5940,7 +5968,7 @@ deepEqual(actual, expected); }); - test('should provide the correct `callback` arguments', 1, function() { + test('should provide the correct `customizer` arguments', 1, function() { var argsList = []; var object1 = { @@ -7682,15 +7710,15 @@ }; var source = { - 'foo': { 'b': { 'foo': { 'c': {} } } }, + 'foo': { 'b': { 'c': { 'd': {} } } }, 'bar': {} }; - source.foo.b.foo.c = source; + source.foo.b.c.d = source; source.bar.b = source.foo.b; 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() { @@ -7735,12 +7763,12 @@ 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); 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) { return _.isArray(a) ? a.concat(b) : undefined; });