From 5fe373f7aa83964a2b413930e92d3ecc2e319ed7 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 21 Mar 2016 20:46:15 -0700 Subject: [PATCH] Add support for deep cloning maps and sets. --- lodash.js | 77 +++++++++++++++++++++++++++++++++++++++------------- test/test.js | 34 +++++++++++++++-------- 2 files changed, 80 insertions(+), 31 deletions(-) diff --git a/lodash.js b/lodash.js index b405e83de..65670256e 100644 --- a/lodash.js +++ b/lodash.js @@ -2437,7 +2437,7 @@ if (!cloneableTags[tag]) { return object ? value : {}; } - result = initCloneByTag(value, tag, isDeep); + result = initCloneByTag(value, tag, baseClone, isDeep); } } // Check for circular references and return its corresponding clone. @@ -2448,11 +2448,18 @@ } stack.set(value, result); + if (!isArr) { + var props = isFull ? getAllKeys(value) : keys(value); + } // Recursively populate clone (susceptible to call stack limits). - (isArr ? arrayEach : baseForOwn)(value, function(subValue, key) { + arrayEach(props || value, function(subValue, key) { + if (props) { + key = subValue; + subValue = value[key]; + } assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack)); }); - return (isFull && !isArr) ? copySymbols(value, result) : result; + return result; } /** @@ -2768,6 +2775,24 @@ return (index && index == length) ? object : undefined; } + /** + * The base implementation of `getAllKeys` and `getAllKeysIn` which uses + * `keysFunc` and `symbolsFunc` to get the enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Function} keysFunc The function to get the keys of `object`. + * @param {Function} symbolsFunc The function to get the symbols of `object`. + * @returns {Array} Returns the array of property names and symbols. + */ + function baseGetAllKeys(object, keysFunc, symbolsFunc) { + var result = keysFunc(object); + return isArray(object) + ? result + : arrayPush(result, symbolsFunc(object)); + } + /** * The base implementation of `_.has` without support for deep paths. * @@ -3176,10 +3201,9 @@ if (object === source) { return; } - var props = (isArray(source) || isTypedArray(source)) - ? undefined - : keysIn(source); - + if (!(isArray(source) || isTypedArray(source))) { + var props = keysIn(source); + } arrayEach(props || source, function(srcValue, key) { if (props) { key = srcValue; @@ -3905,10 +3929,13 @@ * * @private * @param {Object} map The map to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. * @returns {Object} Returns the cloned map. */ - function cloneMap(map) { - return arrayReduce(mapToArray(map), addMapEntry, new map.constructor); + function cloneMap(map, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(mapToArray(map), true) : mapToArray(map); + return arrayReduce(array, addMapEntry, new map.constructor); } /** @@ -3929,10 +3956,13 @@ * * @private * @param {Object} set The set to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. * @returns {Object} Returns the cloned set. */ - function cloneSet(set) { - return arrayReduce(setToArray(set), addSetEntry, new set.constructor); + function cloneSet(set, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(setToArray(set), true) : setToArray(set); + return arrayReduce(array, addSetEntry, new set.constructor); } /** @@ -4957,7 +4987,18 @@ } /** - * Creates an array of the own and inherited enumerable property names and + * Creates an array of own enumerable property names and symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ + function getAllKeys(object) { + return baseGetAllKeys(object, keys, getSymbols); + } + + /** + * Creates an array of own and inherited enumerable property names and * symbols of `object`. * * @private @@ -4965,10 +5006,7 @@ * @returns {Array} Returns the array of property names and symbols. */ function getAllKeysIn(object) { - var result = keysIn(object); - return isArray(object) - ? result - : arrayPush(result, getSymbolsIn(object)); + return baseGetAllKeys(object, keysIn, getSymbolsIn); } /** @@ -5258,10 +5296,11 @@ * @private * @param {Object} object The object to clone. * @param {string} tag The `toStringTag` of the object to clone. + * @param {Function} cloneFunc The function to clone values. * @param {boolean} [isDeep] Specify a deep clone. * @returns {Object} Returns the initialized clone. */ - function initCloneByTag(object, tag, isDeep) { + function initCloneByTag(object, tag, cloneFunc, isDeep) { var Ctor = object.constructor; switch (tag) { case arrayBufferTag: @@ -5277,7 +5316,7 @@ return cloneTypedArray(object, isDeep); case mapTag: - return cloneMap(object); + return cloneMap(object, isDeep, cloneFunc); case numberTag: case stringTag: @@ -5287,7 +5326,7 @@ return cloneRegExp(object); case setTag: - return cloneSet(object); + return cloneSet(object, isDeep, cloneFunc); case symbolTag: return cloneSymbol(object); diff --git a/test/test.js b/test/test.js index 4c0c869e6..d70877c2d 100644 --- a/test/test.js +++ b/test/test.js @@ -50,6 +50,7 @@ create = Object.create, fnToString = funcProto.toString, freeze = Object.freeze, + getSymbols = Object.getOwnPropertySymbols, identity = function(value) { return value; }, JSON = root.JSON, noop = function() {}, @@ -484,7 +485,6 @@ }; }())); - var _getOwnPropertySymbols = Object.getOwnPropertySymbols; setProperty(Object, 'getOwnPropertySymbols', undefined); var _propertyIsEnumerable = objectProto.propertyIsEnumerable; @@ -539,8 +539,8 @@ setProperty(objectProto, 'propertyIsEnumerable', _propertyIsEnumerable); setProperty(root, 'Buffer', Buffer); - if (_getOwnPropertySymbols) { - Object.getOwnPropertySymbols = _getOwnPropertySymbols; + if (getSymbols) { + Object.getOwnPropertySymbols = getSymbols; } else { delete Object.getOwnPropertySymbols; } @@ -2741,24 +2741,34 @@ }); QUnit.test('`_.' + methodName + '` should clone symbol properties', function(assert) { - assert.expect(2); + assert.expect(3); + + function Foo() { + this[symbol] = { 'c': 1 }; + } if (Symbol) { - var object = {}; - object[symbol] = {}; - assert.strictEqual(func(object)[symbol], object[symbol]); + var symbol2 = Symbol('b'); + Foo.prototype[symbol2] = 2; + + var object = { 'a': { 'b': new Foo } }; + object[symbol] = { 'b': 1 }; + + var actual = func(object); + + assert.deepEqual(getSymbols(actual.a.b), [symbol]); if (isDeep) { - object = { 'a': { 'b': {} } }; - object.a.b[symbol] = {}; - assert.strictEqual(func(object).a.b[symbol], object.a.b[symbol]); + assert.deepEqual(actual[symbol], object[symbol]); + assert.deepEqual(actual.a.b[symbol], object.a.b[symbol]); } else { - skipAssert(assert); + assert.strictEqual(actual[symbol], object[symbol]); + assert.strictEqual(actual.a, object.a); } } else { - skipAssert(assert, 2); + skipAssert(assert, 3); } });