From 1b3cb0f18482dcf9ebefcdf149edd648cb402683 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Wed, 30 Sep 2015 21:28:42 -0700 Subject: [PATCH] Combine `MemCache` and `Stack` into `MapCache`. --- lodash.js | 284 ++++++++++++++++++++++++--------------------------- test/test.js | 19 ++-- 2 files changed, 147 insertions(+), 156 deletions(-) diff --git a/lodash.js b/lodash.js index d29362905..3fe17d0c0 100644 --- a/lodash.js +++ b/lodash.js @@ -1358,6 +1358,7 @@ var ArrayBuffer = context.ArrayBuffer, Map = getNative(context, 'Map'), Reflect = context.Reflect, + Set = getNative(context, 'Set'), Symbol = context.Symbol, Uint8Array = context.Uint8Array, WeakMap = getNative(context, 'WeakMap'), @@ -1381,19 +1382,18 @@ nativeMax = Math.max, nativeMin = Math.min, nativeParseInt = context.parseInt, - nativeRandom = Math.random, - nativeSet = getNative(context, 'Set'); + nativeRandom = Math.random; /** Used to store function metadata. */ var metaMap = WeakMap && new WeakMap; /** Used to detect maps and sets. */ var mapCtorString = Map ? fnToString.call(Map) : '', - setCtorString = nativeSet ? fnToString.call(nativeSet) : ''; + setCtorString = Set ? fnToString.call(Set) : ''; /** Detect lack of support for map and set `toStringTag` values (IE 11). */ - var noMapSetTag = Map && nativeSet && - !(objToString.call(new Map) == mapTag && objToString.call(new nativeSet) == setTag); + var noMapSetTag = Map && Set && + !(objToString.call(new Map) == mapTag && objToString.call(new Set) == setTag); /** Used to lookup unminified function names. */ var realNames = {}; @@ -1718,45 +1718,77 @@ /*------------------------------------------------------------------------*/ /** - * Creates a memoize cache object to store key-value pairs. + * Creates a map cache object to store key-value pairs. * * @private - * @static - * @name Cache - * @memberOf _.memoize */ - function MemCache() { - this.__data__ = {}; + function MapCache() { + this.__data__ = { + 'hash': createCache(), + 'map': (Map ? new Map : []), + 'string': createCache() + }; } /** - * Removes `key` and its value from the memoize cache. + * Removes `key` and its value from the map. * * @private * @name delete - * @memberOf _.memoize.Cache + * @memberOf MapCache * @param {string} key The key of the value to remove. - * @returns {boolean} Returns `true` if the entry was removed successfully, else `false`. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. */ - function memDelete(key) { - return this.has(key) && delete this.__data__[key]; + function mapDelete(key) { + var data = this.__data__, + map = data.map; + + if (isKeyable(key)) { + var hash = typeof key == 'string' ? data.string : data.hash; + return hasOwnProperty.call(hash, key) && delete hash[key]; + } + if (Map) { + return map['delete'](key); + } + var index = assocIndexOf(map, key); + if (index < 0) { + return false; + } + var lastIndex = map.length - 1; + if (index == lastIndex) { + map.pop(); + } else { + splice.call(map, index, 1); + } + return true; } /** - * Gets the cached value for `key`. + * Gets the map value for `key`. * * @private * @name get - * @memberOf _.memoize.Cache + * @memberOf MapCache * @param {string} key The key of the value to get. * @returns {*} Returns the cached value. */ - function memGet(key) { - return key == '__proto__' ? undefined : this.__data__[key]; + function mapGet(key) { + var data = this.__data__, + map = data.map; + + if (isKeyable(key)) { + var hash = typeof key == 'string' ? data.string : data.hash; + return (nativeCreate || hasOwnProperty.call(hash, key)) ? hash[key] : undefined; + } + if (Map) { + return map.get(key); + } + var index = assocIndexOf(map, key); + return index < 0 ? undefined : map[index][1]; } /** - * Checks if a cached value for `key` exists. + * Checks if a map value for `key` exists. * * @private * @name has @@ -1764,83 +1796,51 @@ * @param {string} key The key of the entry to check. * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. */ - function memHas(key) { - return key != '__proto__' && hasOwnProperty.call(this.__data__, key); + function mapHas(key) { + var data = this.__data__, + map = data.map; + + if (isKeyable(key)) { + var hash = typeof key == 'string' ? data.string : data.hash; + return hasOwnProperty.call(hash, key); + } + return Map ? map.has(key) : (assocIndexOf(map, key) > -1); } /** - * Sets `value` to `key` of the cache. + * Sets `value` to `key` of the map. * * @private * @name set - * @memberOf _.memoize.Cache + * @memberOf MapCache * @param {string} key The key of the value to set. * @param {*} value The value to set. - * @returns {Object} Returns the memoize cache object. + * @returns {Object} Returns the map cache object. */ - function memSet(key, value) { - if (key != '__proto__') { - this.__data__[key] = value; + function mapSet(key, value) { + var data = this.__data__, + map = data.map; + + if (isKeyable(key)) { + var hash = typeof key == 'string' ? data.string : data.hash; + hash[key] = value; + } + else if (Map) { + map.set(key, value); + } + else { + var index = assocIndexOf(map, key); + if (index < 0) { + map.push([key, value]); + } else { + map[index][1] = value; + } } return this; } /*------------------------------------------------------------------------*/ - /** - * Creates a stack object to store key-value pairs. - * - * @private - */ - function Stack() { - return Map ? new Map : (this.__data__ = [], this); - } - - /** - * Removes `key` and its value from the stack. - * - * @private - * @name delete - * @memberOf Stack - * @param {string} key The key of the value to remove. - * @returns {boolean} Returns `true` if the entry was removed successfully, else `false`. - */ - function stackDelete() { - var data = this.__data__; - return !!data.length && (data.pop(), true); - } - - /** - * Gets the stack value for `key`. - * - * @private - * @name get - * @memberOf Stack - * @param {string} key The key of the value to get. - * @returns {*} Returns the cached value. - */ - function stackGet(key) { - var data = this.__data__, - index = assocIndexOf(data, key); - - return index < 0 ? undefined : data[index][1]; - } - - /** - * Sets `value` to `key` of the stack. - * - * @private - * @name set - * @memberOf Stack - * @param {string} key The key of the value to set. - * @param {*} value The value to set. - */ - function stackSet(key, value) { - this.__data__.push([key, value]); - } - - /*------------------------------------------------------------------------*/ - /** * * Creates a set cache object to store unique values. @@ -1849,17 +1849,17 @@ * @param {Array} [values] The values to cache. */ function SetCache(values) { - var length = values ? values.length : 0; + var length = values ? values.length : 0, + data = this.__data__ = new MapCache; - this.data = { 'hash': nativeCreate(null), 'set': new nativeSet }; while (length--) { - this.push(values[length]); + data.set(values[length], true); } } /** * Checks if `value` is in `cache` mimicking the return signature of - * `_.indexOf` by returning `0` if the value is found, else `-1`. + * `_.indexOf` by returning `0` if `value` is found, else `-1`. * * @private * @param {Object} cache The set cache to search. @@ -1867,10 +1867,7 @@ * @returns {number} Returns `0` if `value` is found, else `-1`. */ function cacheIndexOf(cache, value) { - var data = cache.data, - result = (typeof value == 'string' || isObject(value)) ? data.set.has(value) : data.hash[value]; - - return result ? 0 : -1; + return cache.__data__.has(value) ? 0 : -1; } /** @@ -1882,12 +1879,7 @@ * @param {*} value The value to cache. */ function cachePush(value) { - var data = this.data; - if (typeof value == 'string' || isObject(value)) { - data.set.add(value); - } else { - data.hash[value] = true; - } + this.__data__.set(value, true); } /*------------------------------------------------------------------------*/ @@ -1977,7 +1969,7 @@ } } // Check for circular references and return its corresponding clone. - stack || (stack = new Stack); + stack || (stack = new MapCache); var stacked = stack.get(value); if (stacked) { return stacked; @@ -2047,7 +2039,7 @@ var index = -1, indexOf = getIndexOf(), isCommon = indexOf === baseIndexOf, - cache = (isCommon && values.length >= LARGE_ARRAY_SIZE) ? createCache(values) : null, + cache = (isCommon && values.length >= LARGE_ARRAY_SIZE) ? new SetCache(values) : null, valuesLength = values.length; if (cache) { @@ -2400,7 +2392,7 @@ } // Assume cyclic values are equal. // For more information on detecting circular references see https://es5.github.io/#JO. - stack || (stack = new Stack); + stack || (stack = new MapCache); var stacked = stack.get(object); if (stacked) { return stacked == other; @@ -2452,7 +2444,7 @@ return false; } } else { - var stack = new Stack, + var stack = new MapCache, result = customizer ? customizer(objValue, srcValue, key, object, source, stack) : undefined; if (!(result === undefined ? baseIsEqual(srcValue, objValue, customizer, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG, stack) : result)) { @@ -2605,7 +2597,7 @@ srcValue = source[key]; } if (isObject(srcValue)) { - stack || (stack = new Stack); + stack || (stack = new MapCache); baseMergeDeep(object, source, key, baseMerge, customizer, stack); } else { @@ -2994,7 +2986,7 @@ length = array.length, isCommon = indexOf === baseIndexOf, isLarge = isCommon && length >= LARGE_ARRAY_SIZE, - seen = isLarge ? createCache() : null, + seen = isLarge ? new SetCache : null, result = []; if (seen) { @@ -3432,14 +3424,13 @@ } /** - * Creates a `Set` cache object to optimize linear searches of large arrays. + * Creates an empty cache object. * * @private - * @param {Array} [values] The values to cache. - * @returns {null|Object} Returns the new cache object if `Set` is supported, else `null`. + * @returns {Object} Returns the new cache object. */ - function createCache(values) { - return (nativeCreate && nativeSet) ? new SetCache(values) : null; + function createCache() { + return nativeCreate ? nativeCreate(null) : {}; } /** @@ -4312,6 +4303,17 @@ (object != null && value in Object(object))); } + /** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ + function isKeyable(value) { + return !(value === '__proto__' || isObject(value)); + } + /** * Checks if `func` has a lazy counterpart. * @@ -5069,7 +5071,7 @@ while (othIndex--) { var value = arrays[othIndex] = isArrayLike(value = arrays[othIndex]) ? value : []; - caches[othIndex] = (isCommon && value.length >= 120) ? createCache(othIndex && value) : null; + caches[othIndex] = (isCommon && value.length >= 120) ? new SetCache(othIndex && value) : null; } var array = arrays[0], index = -1, @@ -7759,9 +7761,8 @@ * Creates a function that memoizes the result of `func`. If `resolver` is * provided it determines the cache key for storing the result based on the * arguments provided to the memoized function. By default, the first argument - * provided to the memoized function is coerced to a string and used as the - * cache key. The `func` is invoked with the `this` binding of the memoized - * function. + * provided to the memoized function is used as the map cache key. The `func` + * is invoked with the `this` binding of the memoized function. * * **Note:** The cache is exposed as the `cache` property on the memoized * function. Its creation may be customized by replacing the `_.memoize.Cache` @@ -7776,35 +7777,27 @@ * @returns {Function} Returns the new memoizing function. * @example * - * var upperCase = _.memoize(function(string) { - * return string.toUpperCase(); - * }); + * var object = { 'a': 1, 'b': 2 }; + * var other = { 'c': 3, 'd': 4 }; * - * upperCase('fred'); - * // => 'FRED' + * var values = _.memoize(_.values); + * values(object); + * // => [1, 2] + * + * values(other); + * // => [3, 4] + * + * object.a = 2; + * values(object); + * // => [1, 2] * * // modifying the result cache - * upperCase.cache.set('fred', 'BARNEY'); - * upperCase('fred'); - * // => 'BARNEY' + * values.cache.set(object, ['a', 'b']); + * values(object); + * // => ['a', 'b'] * * // replacing `_.memoize.Cache` - * var object = { 'user': 'fred' }; - * var other = { 'user': 'barney' }; - * var identity = _.memoize(_.identity); - * - * identity(object); - * // => { 'user': 'fred' } - * identity(other); - * // => { 'user': 'fred' } - * * _.memoize.Cache = WeakMap; - * var identity = _.memoize(_.identity); - * - * identity(object); - * // => { 'user': 'fred' } - * identity(other); - * // => { 'user': 'barney' } */ function memoize(func, resolver) { if (typeof func != 'function' || (resolver && typeof resolver != 'function')) { @@ -9740,8 +9733,8 @@ * @returns {boolean} Returns `true` if `path` exists, else `false`. * @example * - * var object = { 'a': { 'b': { 'c': 3 } } }, - * other = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) }); + * var object = { 'a': { 'b': { 'c': 3 } } }; + * var other = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) }); * * _.has(object, 'a'); * // => true @@ -12158,22 +12151,17 @@ LazyWrapper.prototype = baseCreate(baseLodash.prototype); LazyWrapper.prototype.constructor = LazyWrapper; - // Add functions to the `MemCache` cache. - MemCache.prototype['delete'] = memDelete; - MemCache.prototype.get = memGet; - MemCache.prototype.has = memHas; - MemCache.prototype.set = memSet; + // Add functions to the `MapCache` cache. + MapCache.prototype['delete'] = mapDelete; + MapCache.prototype.get = mapGet; + MapCache.prototype.has = mapHas; + MapCache.prototype.set = mapSet; // Add functions to the `Set` cache. SetCache.prototype.push = cachePush; - // Add functions to the `Stack` cache. - Stack.prototype['delete'] = stackDelete; - Stack.prototype.get = stackGet; - Stack.prototype.set = stackSet; - // Assign cache to `_.memoize`. - memoize.Cache = MemCache; + memoize.Cache = MapCache; // Add functions that return wrapped values when chaining. lodash.after = after; diff --git a/test/test.js b/test/test.js index fdde09bd1..0b8acafca 100644 --- a/test/test.js +++ b/test/test.js @@ -12165,17 +12165,19 @@ }); }); - QUnit.test('should skip the `__proto__` key', function(assert) { + QUnit.test('should cache the `__proto__` key', function(assert) { assert.expect(8); + var array = [], + key = '__proto__'; + _.times(2, function(index) { var count = 0, - key = '__proto__', resolver = index && _.identity; var memoized = _.memoize(function() { count++; - return []; + return array; }, resolver); var cache = memoized.cache; @@ -12183,9 +12185,9 @@ memoized(key); memoized(key); - assert.strictEqual(count, 2); - assert.strictEqual(cache.get(key), undefined); - assert.strictEqual(cache['delete'](key), false); + assert.strictEqual(count, 1); + assert.strictEqual(cache.get(key), array); + assert.strictEqual(cache['delete'](key), true); assert.notOk(cache.__data__ instanceof Array); }); }); @@ -12214,9 +12216,10 @@ return true; }, 'get': function(key) { - return _.find(this.__data__, function(entry) { + var entry = _.find(this.__data__, function(entry) { return key === entry.key; - }).value; + }); + return entry && entry.value; }, 'has': function(key) { return _.some(this.__data__, function(entry) {